Skip to content

~5 min readgrounded in docs/DEPLOY.md · scripts/{bootstrap-cloudflare,teardown-cloudflare}.mjs · apps/worker/wrangler.toml · apps/worker/src/index.ts (scheduled) · package.json

Operate·

A deployment is two things you own: a Vercel project and a Cloudflare Worker with its storage. This page is what to do with them after day one.

What runs on its own·

wrangler.toml declares crons = ["* * * * *"]. Every minute the worker's scheduled handler runs, in this order:

  1. Map-presence hygiene — deletes location rows older than LOCATION_SWEEP_AGE_S (24 h); dead clients stop resting on the map.
  2. Scheduled jobs — runDueJobs: fires due jobs with the compare-and-swap guard and the 24-hour catch-up. Scheduling jobs.
  3. Telegram — long-polls getUpdates for every linked bot; replies go through /api/job-run.
  4. Tool-update sweep — weekly, self-gated through KV; notify-only.
  5. Payments reconciliation — only when PAYMENTS_ENABLED="true": payer half, receiver half, then the pager strictly after both. Each is caught so none can take down job dispatch.

Nothing else is background. The app has no cron; everything that must happen while nobody is looking happens in the worker.

Where to look·

Health first. GET /api/health on the app and GET /health on the worker report configuration as booleans (workerConfigured, appUrlConfigured, paymentsEnabled, emailForwardConfigured) and never leak a value.

Worker logs. npx wrangler tail --config wrangler.generated.toml from apps/worker streams every request and cron tick live, including the console.log lines the handlers write on caught errors ('shares insert', 'runDueJobs job'…). Cloudflare's dashboard keeps the same stream under Workers → Logs.

App logs. Vercel → project → Logs, or npx vercel logs <deployment-url>. The two lines worth grepping: TINY_WORKER_URL not set (a missing variable on the environment you deployed to — check npx vercel env ls production) and any 401 from the worker (an INTERNAL_API_KEY mismatch).

Storage. npx wrangler d1 execute <db> --config wrangler.generated.toml --command "SELECT count(*) FROM users" for a quick look at D1; the names of your namespaces, indexes and bucket are all in docs/PROVISIONED.md, written by the bootstrap script.

Errors you will actually see·

The short list. The FAQ has every error string the routes and worker emit, with the file each comes from.

Symptom Cause Fix
BLOCKED — the commit author doesn't have permission to create deployments CLI deploys carry the HEAD commit's author; a team with Git author permission refuses non-members. The CLI shows UNKNOWN and hangs commit as a team member or deploy from a Git connection; vercel ls and /v6/deployments show the real reason
workerConfigured: false · TINY_WORKER_URL not set the variable is missing on that Vercel environment npx vercel env add TINY_WORKER_URL production, redeploy
every worker call 401 INTERNAL_API_KEY differs between the app and wrangler secret put set the same value on both, redeploy the app
Vectorize: filter on unindexed property the name / userId metadata indexes were not created re-run node scripts/bootstrap-cloudflare.mjs — idempotent, adds them
WebAuthn "invalid RP ID" / OAuth returns to the wrong host NEXT_PUBLIC_APP_URL does not match the origin you are on set it to the exact public origin, redeploy
/pay/* and /api/wallet* answer 404 payments are off — the default intended; PAYMENTS_ENABLED="true" on both sides turns them on

Rotating secrets·

Secret Effect of rotating
AUTH_JWT_SECRET every session cookie invalid — browsers and devices sign in again; nothing else
INTERNAL_API_KEY change it on the app and wrangler secret put INTERNAL_API_KEY together, then redeploy the app; between the two, worker calls 401. If MODEL_CONFIG_ENC_KEY is unset, users' stored provider keys were encrypted with the old value — set a dedicated MODEL_CONFIG_ENC_KEY before you rotate
ENROLL_SECRET outstanding enrollment codes stop verifying; enrolled devices keep their tokens
VAPID_* browsers with a subscription made under the old key hit InvalidStateError on next subscribe; the client drops it and re-subscribes automatically
GITHUB_CLIENT_SECRET update in the OAuth App and on Vercel together

Worker secrets: npx wrangler secret put <NAME> --config wrangler.generated.toml. App secrets: printf '%s' <value> | npx vercel env add <NAME> production --force, then npx vercel redeploy.

Updating·

The repo is a normal monorepo: npm run typecheck, npm run lint, npm run test (web + worker + examples/), npm run build at the root run across both workspaces. Deploy the worker with npx wrangler deploy --config wrangler.generated.toml from apps/worker; the app deploys on push through your Vercel Git connection or npx vercel deploy --prod. D1 migrations live in apps/worker/migrations/ and are applied by bootstrap-cloudflare.mjs --migrate (or wrangler d1 migrations apply).

Tearing down·

node scripts/teardown-cloudflare.mjs --yes           # deletes exactly what docs/PROVISIONED.md lists
node scripts/teardown-cloudflare.mjs --yes --keep-data   # deletes the worker, keeps KV/D1/Vectorize/R2
npx vercel project rm <your-project>

The teardown script reads the ledger the bootstrap wrote, deletes only names carrying your prefix, and leaves what it cannot delete (the Vercel project, the GitHub OAuth App) listed as manual steps. Both scripts have been rehearsed both ways: an empty account to a serving worker in about half a minute, and back.