Security checklist
What is already enforced, and what you must do before going live.
Before launch
JWT_SECRETis a freshopenssl rand -base64 32— not the one from any example, demo or repository.envis not committed, or the repository is private, or production values live only in the host's settings- The Atlas database user has a strong password and the IP allowlist is not
0.0.0.0/0 /setuphas been completed — no unclaimed-administrator window is openCLIENT_URLandADMIN_URLare the exact production origins (the CORS allowlist is strict)- OAuth callback URLs point only at your production API
- Stripe is on live keys and the webhook secret matches the live endpoint
- The
previewdemo account, if you keep one, has a password nobody would guess NODE_ENV=productionon the API
Already enforced in code
- Every write endpoint chains
protect→ role gate →preventReadOnlyActions. - Signups cannot choose a role; the body's
roleis never read. - The JWT never reaches client JavaScript — httpOnly, SameSite=Lax cookie only.
?next=andRefererredirects are reduced to same-origin paths — no open redirect.- Order prices come from the catalogue; client prices and totals are ignored. Stock is reduced atomically.
- Stripe webhooks are signature-verified over the raw body.
- OAuth runs on the API with signed state and PKCE; Google ID tokens are verified against Google's JWKS — issuer, audience,
email_verified. - Uploads are authenticated and scoped under your media root folder; the
cleanupandbulk-deletetools are admin-only. - Passwords are stored only as bcrypt hashes; new email sign-ups verify their address before they can order.
- Helmet, rate limiting,
express-mongo-sanitizeandhppare on. Auth middleware never logs headers.
The full record of what was audited and fixed is in apps/api/SECURITY-AUDIT.md.
Known limits
- No refresh-token rotation — a 30-day access token is the whole session. Revoking a role takes effect on the next request because
protectreloads the user, but a stolen token is valid until expiry. - Social providers you have not configured stay hidden — there is no half-working button.