Security
Security is built into the foundation of this kit, not bolted on. This is a rundown of what ships enabled by default — including the subtle correctness guarantees that are easy to get wrong and expensive to discover in production.
Authentication & sessions
- HttpOnly session cookies. The
__sessioncookie isHttpOnly,Secure(in production), andSameSite=Lax, so client-side scripts cannot read it and browsers withhold it from most cross-site requests. Mutating cookie-auth routes also validate the request origin;SameSiteis defense-in-depth, not the only CSRF control. - Revocation-aware. Every protected request re-verifies the session against the Firebase Admin SDK with
checkRevoked=true, so deactivating or signing a user out takes effect on their next request — not whenever a token happens to expire. - One account per email. With Firebase's required default one-account-per-email setting, Google and email-OTP resolve to one identity. The app additionally refuses to create a second application profile for an email if provider linking is misconfigured or cannot be resolved safely.
- Brute-force limits. OTP verification is capped per code (attempts tracked inside a transaction) and per IP (10/min); OTP sends are capped at 10/hour/IP. Codes are stored only as salted SHA-256 hashes and are never readable by clients.
Authorization & entitlements
- Entitlements are server-only.
roleandpricingTiercan only ever be written by the server (Admin SDK) — Firestore rules make them unwritable from the client, so a signed-in user can never grant themselves admin or Pro. - Stripe is the single source of truth for billing. Access is derived from live, webhook-authoritative Stripe subscription state (and continuously reconciled), never from a client claim.
- Admin bootstrap is verified-email-gated. The
SUPER_ADMIN_EMAILSauto-grant applies only to a provider-verified email. - The last usable admin can't be removed. Every path that would strip admin access — an admin deleting, demoting, or deactivating another admin, and an admin deleting their own account through account settings — is guarded so the app can never be left with zero usable admins. "Usable" means role admin, active, and not mid-deletion, matching the Firestore
isAdmin()rule.
Firestore & Storage rules
- Pre-configured, tested rules enforce that users read/write only their own documents, that entitlement/role fields are server-only, and that internal collections (OTP hashes, audit logs, billing state, deletion jobs) are neither readable nor writable by clients.
- Default-deny is in place and Storage is locked down. Rules live in
firestore.rules/storage.rules(deploy via the Firebase CLI). A rules test suite runs against the Firebase emulator vianpm run test:rules.
API & network
- Edge route guard.
proxy.tsredirects unauthenticated requests away from protected routes before they reach a function; full verification still happens server-side. - CSRF protection. Cookie-authenticated mutating routes origin-validate requests (
lib/csrf.ts) and fail closed; Bearer-only API calls require anAuthorizationheader, which a cross-site form cannot attach. Combined withSameSite=Lax, a cross-site page can't act on a user's session. - Rate limiting, on by default. Sensitive endpoints (OTP, contact, export) use durable, fail-closed, Firestore-backed limits. Limiting is enabled in every environment unless you explicitly set
RATE_LIMIT_DISABLED=true— it is never silently switched off by a misconfiguredNODE_ENV. - Input validation. Routes validate and sanitize input with Zod (
lib/validators.ts). - No internal error leakage. Public endpoints return generic messages; the underlying cause stays in the server logs.
Response headers
next.config.mjs sets a hardened header set on every response:
Content-Security-Policy— includingframe-ancestors 'none',base-uri 'self',form-action 'self',object-src 'none', andscript-src-attr 'none'.script-srcstill allows inline script elements for Next/theme JSON-LD compatibility; migrate to nonces or hashes before claiming a nonce-strict CSP.Strict-Transport-Security(HSTS, 2 years,preload)X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy,Permissions-Policy
Data lifecycle & privacy
- Durable account deletion. Deletion makes active or ambiguous Checkout creation terminal and cancels Stripe before disabling Auth, then writes a tombstone and removes data through a resumable, leased job. Failed provider steps leave the profile recoverable and are retried by the authenticated scheduled recovery route; delayed Stripe events cannot recreate it.
- GDPR. One-click data export, a cookie-consent banner, and privacy/terms templates are included.
- PII-redacting logs. Emails are masked and secret-like fields are dropped before anything is written to logs.
Operations
- Secrets (Firebase private key, Stripe secret,
CRON_SECRET) live in the host's environment, never in git. - Timing-safe secret checks. The cron secret is compared with a constant-time digest comparison.
- Error alerting. Unhandled server errors fire a throttled, deduplicated ops alert (email + an
ops_alertsrecord) through theonRequestErrorinstrumentation hook — and it's ready to run alongside a full error monitor such as Sentry. - Dependency gate. Security-sensitive dependency state and the remaining release blocker are tracked in DEPENDENCY_AUDIT.md.
Next: Read about Customization.