Deploying
Three deployable parts, none of them exotic:
| Part | What it is | Where it runs |
|---|---|---|
apps/site |
The public site and this documentation — static HTML | Cloudflare Pages on vercatus.com |
| API | Hono server; also serves the built workspace from apps/web/dist |
A container, ROLE=api, on app.vercatus.com |
| Worker | Claims queued runs and crawls; needs Chromium | The same image, ROLE=worker, one or more instances |
| Postgres 17 + pgvector | The only stateful service | Neon, Railway Postgres, or any Postgres with the vector extension |
The repository’s Dockerfile builds one image from Playwright’s base (Chromium and its libraries included)
and runs either role by the ROLE variable. .github/workflows/ci.yml typechecks, pushes the schema
into a throwaway Postgres, seeds it, builds the site and runs the tests on every push.
The public site
Section titled “The public site”Cloudflare Workers (static assets), connected to the repository — this is what serves vercatus.com:
- Root directory:
apps/site(thewrangler.jsoncthere names the worker and points it atdist) - Build command:
npm ci && npm run build· Deploy command:npx wrangler deploy - Build watch paths:
apps/site/**, so app commits do not rebuild the site - Variables:
NODE_VERSION=22,SITE_URL=https://vercatus.com - Custom domains:
vercatus.comandwww.vercatus.com
Cloudflare Pages works with the same repository too (root directory empty, build
npm ci && npm -w apps/site run build, output apps/site/dist); the Workers flow is what Cloudflare
creates by default now.
apps/site/public/_headers sets the content type on /.well-known/http-message-signatures-directory,
which is the crawler’s published key. robots.txt and llms.txt ship with the site.
The application
Section titled “The application”-
Postgres. Create a database with the
vectorextension available (create extension vectorruns on firstdb:push). Note itsDATABASE_URL. -
API service from the Dockerfile with these variables:
ROLE=apiRUN_MODE=workerAUTH_MODE=multiAPP_URL=https://app.vercatus.comSESSION_SECRET=<32+ random bytes>DATABASE_URL=postgres://…ANTHROPIC_API_KEY=… # platform key; BYOK workspaces add their own laterANTHROPIC_WORKSPACE_ID=… # if the key is org-scopedCRUX_API_KEY=… # optional, real-user Core Web VitalsWEB_BOT_AUTH_DIRECTORY=https://vercatus.comWEB_BOT_AUTH_PRIVATE_KEY_PATH=/secrets/crawler-ed25519.pem # mount the key; never bake it into the imageREQUIRE_DOMAIN_VERIFICATION=trueLLM_PROFILE=fullExpose port 4180; point
app.vercatus.comat it. -
Worker service from the same image with the same variables and
ROLE=worker,WORKER_CONCURRENCY=2. Give it more memory than the API — Chromium is the cost — and scale by adding instances; each run is claimed by exactly one. -
Once per deploy (a release command, or a one-off shell):
npm run db:push && API_DB_PASSWORD=… npm run db:rls && npm run seed.db:pushcreates thevectorextension and applies the schema;db:rlsapplies row-level security and creates thevercatus_apirole;seedupserts the plans. All three are safe to repeat. -
Connect the API as
vercatus_api, not as the owner: itsDATABASE_URLuses that role and password. The worker and one-off commands keep the owner URL. With that split, a query that ever missed its organisation filter returns nothing rather than another tenant’s rows.
The crawler signs every request with the key at WEB_BOT_AUTH_PRIVATE_KEY_PATH; the matching public
key is the one published on the site. Generate a new pair with npm run ca -- keygen <path> and update
both places together.
What is not in place yet
Section titled “What is not in place yet”Object storage for page bodies (they are written to .cache on the worker’s disk today, so a worker
needs a persistent volume or accepts re-fetching after a restart), structured error reporting, and a
status page. See the product plan.