Local development
Requirements:
- Node.js 22.22 or newer
- npm
- No Cloudflare account for local development
npm install
cp .dev.vars.example .dev.vars
npm run migrate
npm run seed
npm run dev
Open http://localhost:5173. Use /demo for seeded one-click sessions, or
/login to generate a development magic link.
Seeded accounts:
admin@callboard.devspeaker@callboard.dev
Deployment
Authenticate Wrangler, create or select D1 and R2 resources, configure
wrangler.jsonc, and set secrets:
npx wrangler login
npx wrangler d1 create callboard-db
npx wrangler r2 bucket create callboard-files
npx wrangler secret put SESSION_SECRET
npx wrangler secret put MAGIC_LINK_SECRET
npx wrangler secret put RATE_LIMIT_SECRET
npx wrangler secret put RESEND_API_KEY
npm run deploy
Set APP_URL to the deployed origin, CALLBOARD_URL to its public
URL, and CALLBOARD_SMOKE_PROFILE=production. The checked-in
wrangler.jsonc sets DEPLOYMENT_PROFILE=production and DEMO_MODE=0;
the application also requires both values to opt into demo authentication, so a
stray DEMO_MODE=1 cannot mint a seeded admin session in production.
Disposable judge demo
The judge demo is a separate deployment, not an environment toggle on production:
Create a dedicated D1 database and R2 bucket. Do not reuse the production IDs, names, data, or secrets.
Copy
wrangler.demo.example.jsoncto the gitignoredwrangler.demo.jsonc.Replace every
REPLACE_*placeholder with the disposable demo origin, newly provisioned D1 ID, and an ISO-8601DEMO_EXPIRES_ATno more than seven days away. KeepDEPLOYMENT_PROFILE=demo,DEMO_MODE=1, andMAIL_DRIVER=console. The one-click route fails closed when the deadline is missing, malformed, or passed.Set fresh demo-only
SESSION_SECRET,MAGIC_LINK_SECRET, andRATE_LIMIT_SECRETvalues withnpx wrangler secret put SECRET_NAME --config wrangler.demo.jsonc(once per secret, substituting each name).Preview the guarded resource targets, then execute the reset:
npm run demo:reset -- --config=wrangler.demo.jsonc npm run demo:reset -- --config=wrangler.demo.jsonc --executeThe execute path applies remote migrations first, deletes every R2 object recorded in D1 before wiping mutable rows, and restores the deterministic seed last. Do not run a separate post-reset migration or seed; the successful reset output is the migration-and-seed evidence for the final demo state.
Deploy only through the guarded wrapper, then smoke the exact configured origin:
npm run demo:deploy -- --config=wrangler.demo.jsonc npm run smoke:demo -- "$DEMO_URL"The wrapper accepts no implicit/default config or extra Wrangler overrides. It requires the regular, non-symlink
wrangler.demo.jsoncat the repository root, parses comments and trailing commas consistently, rejects ambiguous duplicate safety keys and any Worker/origin/D1/R2 identity shared with the default config, reruns the profile/expiry/resource/mail checks, and invokes Wrangler with that exact config only after validation. Never usenpm run deployfor the disposable demo: its production predeploy path is intentionally tied to the defaultwrangler.jsoncandcallboard-db.
The reset command refuses the production config, non-disposable resource names, real email, unresolved placeholders, expired deadlines, and lifetimes over seven days. Runtime expiry returns 404 from the entire demo Worker; deleting the Worker and dedicated D1/R2 resources after judging is still an operator-owned Cloudflare cleanup step. The repository does not provision or edge-rate-limit those resources, so do not describe a disposable demo as ready until provisioning, reset output, deployed smoke, access verification, and teardown ownership are recorded.
Verification
npm run check # types + repository guards + Vitest
npm run build # production Worker/client build
npm run e2e # seeds first, then Playwright
npm run release:verify # complete local release gate
Playwright refuses every non-loopback CALLBOARD_E2E_URL. The suite mutates
data and uploads files, and a local process cannot prove that a remote Worker
has disabled real email. Run it only against a disposable local server.
npm run release:verify runs the production build, unit/guard/type checks,
migration-drift check, Playwright, a high/critical lockfile dependency audit,
and a guard that rejects mutable GitHub Action references. The same gate is
committed as a GitHub Actions workflow in .github/workflows/check.yml.
Generated migrations must be committed.
Both deployed smoke profiles call /ready. The probe returns 503 unless
SESSION_SECRET, MAGIC_LINK_SECRET, and RATE_LIMIT_SECRET are present
and the D1 binding exposes the complete rate_limit_windows schema. It uses a
zero-row D1 query and does not create sessions, tokens, limiter windows, or demo
data. A passing smoke is repository evidence about those dependencies only; it
is not deployment, walkthrough, or external-secret-scan evidence.
Commands
| Command | Purpose |
|---|---|
npm run dev |
Start the local Vite/workerd server |
npm run check |
Typecheck, repository guards, and unit tests |
npm run build |
Build the production Worker and client bundles |
npm run e2e |
Seed local D1 and run Playwright |
npm run release:verify |
Run checks, build, migration drift, and E2E |
npm run db:generate |
Generate committed Drizzle migrations |
npm run migrate |
Apply local D1 migrations |
npm run seed |
Seed the demo event |
npm run demo:reset -- --config=wrangler.demo.jsonc |
Dry-run a guarded disposable-demo reset; add --execute after review |
npm run demo:deploy -- --config=wrangler.demo.jsonc |
Validate and deploy only the repository-root disposable-demo config |
npm run preview |
Upload a Cloudflare preview version |
npm run deploy |
Check, build, migrate remote D1, deploy, and smoke |
npm run smoke:demo -- "$DEMO_URL" |
Verify read-only runtime readiness, seeded judge data, and demo sign-in |
npm run smoke:production -- "$APP_URL" |
Verify read-only runtime readiness and prove demo sign-in is disabled |