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
Email 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

  1. Apply web/db/schema.sql and then every web/db/migrations/*.sql in order.
  2. npx wrangler queues create offense-assessments
  3. Set the Worker secrets: PLATFORM_OPENROUTER_KEY, CREDENTIALS_KEY (base64, 32 bytes), OFFENSE_RUNNER_TOKEN, and the Stripe secrets.
  4. Optionally bind the private R2 bucket as EVIDENCE.
  5. Configure email.
  6. Deploy the runner with a scoped Boat key, plus BOAT_KEY_SCOPED=1 and RUNNER_REQUIRE_SCOPED_BOAT_KEY=1.
  7. 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_dev is on for the web app; custom domains come from the zone routes.
  • Crons: */5 * * * * (queue recovery, loop dispatch, nudge sweep) and 0 4 * * * (retention purge).
  • The RUNNER service binding points at offense-runner. Set ASSESSMENT_RUNNER=simulated only 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.mjs in docs-site/ (renders content/ into dist/)
  • Served by: the offense-docs Worker, static assets only, no server logic
  • Domain: docs.offense.sh, routed through the offense.sh zone
  • Deploy: .github/workflows/deploy-docs.yml on a push to main that touches docs-site/**, or a manual wrangler deploy from docs-site/

Because the site is static, a docs change cannot take the app down, and the Worker has no bindings to leak.