The repository deploys four things: the web Worker, the runner container, the static landing page, and this docs site. Everything runs on Cloudflare.
What you need
| Piece | Requirement |
|---|---|
| Web app | Cloudflare Workers + a MySQL database (PlanetScale) behind Hyperdrive |
| Queue | offense-assessments |
| Runner | A Cloudflare Container Worker, plus a Boat API key scoped to sandbox operations |
| Models | PLATFORM_OPENROUTER_KEY, or a workspace BYOK key |
RESEND_API_KEY + EMAIL_FROM, or EMAIL_WEBHOOK_URL, or EMAIL_PROVIDER=log in dev |
|
| Evidence (optional) | A private R2 bucket bound as EVIDENCE; without it evidence uses the database |
Deploy order
- Apply
web/db/schema.sqland then everyweb/db/migrations/*.sqlin order. npx wrangler queues create offense-assessments- Set the Worker secrets:
PLATFORM_OPENROUTER_KEY,CREDENTIALS_KEY(base64, 32 bytes),OFFENSE_RUNNER_TOKEN, and the Stripe secrets. - Optionally bind the private R2 bucket as
EVIDENCE. - Configure email.
- Deploy the runner with a scoped Boat key, plus
BOAT_KEY_SCOPED=1andRUNNER_REQUIRE_SCOPED_BOAT_KEY=1. - Run the acceptance scenario.
Do not run step 6 with an account-wide Boat key. The runner needs create, read, command, file and delete on sandboxes, and nothing else.
The acceptance scenario
A deployment is not "done" because the Worker returns 200. The scenario to run:
two workspaces, payment plus a duplicate webhook, two session identities, queued jobs, a runner restart, evidence download, a patched-fixture retest, ledger reconciliation, cleanup, and a low-credit email.
Worker configuration worth knowing
workers_devis on for the web app; custom domains come from the zone routes.- Crons:
*/5 * * * *(queue recovery, loop dispatch, nudge sweep) and0 4 * * *(retention purge). - The
RUNNERservice binding points atoffense-runner. SetASSESSMENT_RUNNER=simulatedonly for local dev — it is labelled as simulated in the UI precisely so a demo is never mistaken for a real run. MAX_ACTIVE_SANDBOXES(10),MAX_GLOBAL_ACTIVE(20),MAX_SANDBOX_BATCH(5),MAX_SWARM_AGENTS(100) bound concurrency and fan-out.
Staging
Staging is a separate Worker (offense-web-staging) with no production
bindings, keys or data. See docs/runbooks/staging.md in the repository for
the one-time Cloudflare setup and the smoke test.
CI
CI runs static checks, the full test suite, and wrangler deploy --dry-run for
every deployable config on each pull request. The ci job is the check to mark
required on main.
Production deploy is gated on the repository variable STAGING_DEPLOY_ENABLED,
which acts as a cheap off switch: if a bad Cloudflare token or config lands on
main, setting it to false stops the deploy without a revert.
This docs site
- Sources:
docs-site/content/*.md - Build:
node build.mjsindocs-site/(renderscontent/intodist/) - Served by: the
offense-docsWorker, static assets only, no server logic - Domain:
docs.offense.sh, routed through theoffense.shzone - Deploy:
.github/workflows/deploy-docs.ymlon a push tomainthat touchesdocs-site/**, or a manualwrangler deployfromdocs-site/
Because the site is static, a docs change cannot take the app down, and the Worker has no bindings to leak.