ShipSecurely — Pre-launch security checklist
Printable
Pre-launch security checklist
A single pass over the issues that most often ship in an AI-built app, before you send the URL to anyone else.
Last updated
Print this page and tick the boxes, or paste the prompt at the bottom into the coding agent that built the app. Each line links to a longer guide. This is a practical list, not a penetration test and not legal advice. Do the checks on a project you own.
Data access
Every Supabase table the API can see has row level security on, and you can point at the policy that decides who reads a row.
Firestore and Storage rules are not test mode and not allow-if-true. A signed-in user cannot read another user's documents.
User uploads sit in a private bucket. A logged-out browser cannot open another user's file URL.
Changing an id in the URL, as a second test user, does not return the first user's data.
Signing out and requesting a private API path returns 401. Hiding the button is not the only check.
Every Server Action that writes data loads the session inside the action and ignores user ids from the form.
Secrets
The production JavaScript bundle does not contain a Supabase secret key, a Stripe secret key, or a model API key.
Git history does not contain a real .env or a private key. Anything that did has been rotated.
Requests for /.env and any debug route do not return configuration or secrets.
Money and abuse
The Stripe webhook rejects requests that fail signature verification and does not mark an order paid from unsigned JSON.
Checkout uses a price id from the server. The browser cannot set the amount, the plan, or an admin role.
Model calls require a signed-in user, have a per-user daily cap, and the provider dashboard has a monthly spend limit.
Login, password reset, and paid API routes return 429 when a caller repeats them too fast.
Injection and the browser
User input is not concatenated into SQL or into a Supabase .or() filter string.
User and model text is rendered as text, or sanitized with a maintained allowlist. No homemade HTML filter.
Tools the model can call check the session on the server. The model does not decide who the user is.
Uploads have a server-side size and type limit, a generated filename, and are not served as HTML or SVG from your origin.
Cookie-authenticated APIs do not reflect arbitrary Origin headers. There is an allowlist, or no CORS middleware at all.
Responses send nosniff, a referrer policy, and frame protection. HSTS is only set if the site is HTTPS-only.
Accounts and launch
Password reset uses the auth provider's email flow, or a long single-use token. It does not update a password from an email alone.
The lockfile is committed, and you have read the current dependency audit instead of force-upgrading.
Privacy and terms pages exist, name the processors you actually use, and point at a contact address that reaches you.
Give the whole list to your agent
You are doing a pre-launch security pass on this repository. Do not add features. Do not claim the app is secure. Work through the list below and change code only where you can see the problem. If you are unsure, write a question instead of weakening a control. Check and fix what you can: 1. Supabase: enable RLS on public tables and replace policies of using (true). Do not use the secret key in client code or NEXT_PUBLIC_ variables. 2. Firebase: remove allow read, write if true. Scope rules to request.auth.uid and the owner field. 3. Storage: private buckets, owner-only prefixes, no user HTML or SVG served from this origin. 4. Every Route Handler and Server Action that reads or writes data must load the session and scope queries to that user. Ignore userId, role, price, and plan from the client. 5. Stripe webhooks must verify the signature on the raw body. Prices must come from server-side price ids. Only the verified webhook grants a plan. 6. Model routes must keep the provider key on the server, require a user, and enforce a small per-user daily cap. Tell me to set a spend limit in the provider dashboard. 7. Add server-side rate limits on login, password reset, and paid routes. 8. Parameterize SQL. Render user and model text as text. Allowlist CORS origins. Add nosniff, referrer policy, and frame-ancestors none. 9. Password reset must not set a password from an email alone. 10. Do not print secrets. Tell me which keys I must rotate if they were committed or shipped to the browser. End with a checklist of what you changed, what you could not verify from the repo, and what I should click-test.