# Security and privacy checklist

## Before any public launch

- Set four different random secrets, each at least 32 bytes.
- Keep `ALLOW_CONSOLE_OTP=false` in production.
- Set `TURNSTILE_REQUIRED=true` and test server-side validation.
- Confirm Cloudflare does not cache `/api/*`, `/q/*`, `/connect/*`, `/access/*`, `/login`, `/onboarding`, `/dashboard/*` or `/admin/*`.
- Use Cloudflare SSL/TLS Full (strict) after the cPanel origin certificate is valid.
- Keep the R2 bucket private; use only presigned operations for originals.
- Restrict R2 CORS to the exact staging or production origins.
- Protect activation CSV exports and delete working copies after activation cards are produced.
- Back up MySQL outside the server and test restoration.
- Keep application, database and R2 credentials separate from BrandPortal.
- Test suspension, replacement, deletion and ownership-transfer procedures before selling physical plaques.

## Never do

- Never print the private activation code on the public plaque.
- Never store an activation code or session token as plaintext.
- Never point a manufactured QR directly to a changeable story slug.
- Never use Cache Everything across authenticated or QR resolver routes.
- Never expose the MySQL database or R2 credentials to browser JavaScript.
- Never enable public R2 access for family originals.

## Upload completion verification

Uploaded media is checked with an R2 `HeadObject` request before the database marks it ready. The stored object size and content type must match the upload authorisation. This prevents an incomplete or substituted object from becoming a published cover.

## Media-key privacy

Original upload filenames are retained only in MySQL for the Story Keeper. R2 object keys use random UUIDs plus an approved image extension, so filenames are not exposed inside short-lived signed media URLs.
