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.
| Context | Mechanism |
|---|---|
| Local Workers dev | .dev.vars — gitignored |
| Local tooling | .env.local — gitignored |
| Deployed Workers | wrangler secret put |
| CI | GitHub 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
usersrow 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
sqltemplates 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.
playersmay 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.