Skip to content

Security and secrets

Security and secrets

Secrets

Never commit a secret. Not to a private repo, not temporarily, not in a test fixture, not in a code comment. Git history is effectively permanent, and a secret in history is a secret that has to be rotated.

ContextMechanism
Local Workers dev.dev.vars — gitignored
Local tooling.env.local — gitignored
Deployed Workerswrangler secret put
CIGitHub repository secrets

Secret vs var: anything that would be damaging if read goes in a secret. Non-sensitive config — a public base URL, a feature flag — goes in vars in wrangler.jsonc, where it is visible and reviewable.

Environment variables on Workers are runtime-only, and process.env stays empty unless compatibility_date is 2025-04-01 or later (ADR-0004). Config read at module scope will be undefined — read it inside the handler.

If a secret is ever committed: rotate it first, then clean the history. In that order.

Rate limiting is a cost control

Every public endpoint is rate limited, shipped in the first commit of apps/api (WI-012).

Cloudflare has no spend protection by default. An unthrottled endpoint that gets hammered blows the daily cap and forces a mid-month upgrade — so this is a budget requirement first and an abuse mitigation second. Both matter; the budget one is what makes it non-deferrable.

Auth

  • Clerk validates; the local users row authorises — ADR-0005.
  • Authorisation checks reference the local user UUID, never clerk_user_id.
  • Verify the session token on every request. Never trust a client-supplied user ID.
  • Clerk webhooks are signed. Verify the signature before acting; treat webhooks as reconciliation, never as the primary path.
  • MFA and passkeys are Clerk Pro features. Social login and the prebuilt UI are free.

Input handling

  • Parse untrusted input with Zod at the boundary, once.
  • Drizzle parameterises queries; hand-written sql templates must interpolate through the template tag, never through string concatenation.
  • User-supplied content — reviews, list descriptions — is escaped on output. Astro escapes by default; anything using a raw HTML directive needs a reason in a comment.

Uploads and storage

  • Serve images from R2, never as CDN-cached large files. Cloudflare’s CDN terms prohibit video and large-file distribution, with a 512 MB per-file cache limit.
  • Validate content type and size server-side. A client-side check is a UX affordance, not a control.
  • Never serve a user upload from the same origin with a user-controlled content type.

Data protection posture

  • Clerk stores user data in the US, with no EU data-residency option. Irrelevant at friends-and-family scale; revisit before any meaningful UK/EU growth. This is a known accepted risk, recorded in ADR-0005.
  • players may describe real people who never consented to an account — per ADR-0006 they are created by whoever logged the session. Keep the record minimal: a display name and nothing more until the row is claimed.
  • Deleting a user must not orphan sessions that other people participated in. Anonymise the participant, keep the session.

Dependencies

  • Adding a dependency that duplicates something already in the stack needs an ADR — decisions.md.
  • Bundle size is a security surface as well as a CPU cost. Gate step 5 watches it.
  • Dependency updates go through the gate like anything else. A green gate is the merge condition, not the changelog.