# 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.

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. Educational guidance, not a security audit. 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. (Supabase tables without row level security: https://shipsecurely.dev/issues/supabase-tables-without-rls.md)
- [ ] Firestore and Storage rules are not test mode and not allow-if-true. A signed-in user cannot read another user's documents. (Firebase rules left open: https://shipsecurely.dev/issues/firebase-rules-left-open.md)
- [ ] User uploads sit in a private bucket. A logged-out browser cannot open another user's file URL. (Open storage buckets: https://shipsecurely.dev/issues/open-storage-buckets.md)
- [ ] Changing an id in the URL, as a second test user, does not return the first user's data. (IDOR: reading or changing someone else's data: https://shipsecurely.dev/issues/idor.md)
- [ ] Signing out and requesting a private API path returns 401. Hiding the button is not the only check. (Auth checked only in the UI: https://shipsecurely.dev/issues/auth-checked-only-in-the-ui.md)
- [ ] Every Server Action that writes data loads the session inside the action and ignores user ids from the form. (Server Actions with no auth check: https://shipsecurely.dev/issues/server-actions-without-auth.md)

## Secrets

- [ ] The production JavaScript bundle does not contain a Supabase secret key, a Stripe secret key, or a model API key. (Secret keys shipped in the frontend: https://shipsecurely.dev/issues/secret-keys-in-the-frontend.md)
- [ ] Git history does not contain a real .env or a private key. Anything that did has been rotated. (Keys committed to GitHub: https://shipsecurely.dev/issues/secrets-committed-to-git.md)
- [ ] Requests for /.env and any debug route do not return configuration or secrets. (Exposed .env files and debug endpoints: https://shipsecurely.dev/issues/exposed-env-and-debug-endpoints.md)

## Money and abuse

- [ ] The Stripe webhook rejects requests that fail signature verification and does not mark an order paid from unsigned JSON. (Unverified Stripe webhooks: https://shipsecurely.dev/issues/unverified-stripe-webhooks.md)
- [ ] Checkout uses a price id from the server. The browser cannot set the amount, the plan, or an admin role. (The client sets the price, role, or plan: https://shipsecurely.dev/issues/client-sets-price-or-role.md)
- [ ] Model calls require a signed-in user, have a per-user daily cap, and the provider dashboard has a monthly spend limit. (No cap on LLM API spend: https://shipsecurely.dev/issues/unbounded-llm-spend.md)
- [ ] Login, password reset, and paid API routes return 429 when a caller repeats them too fast. (No rate limiting on auth or paid API routes: https://shipsecurely.dev/issues/no-rate-limiting.md)

## Injection and the browser

- [ ] User input is not concatenated into SQL or into a Supabase .or() filter string. (SQL injection: https://shipsecurely.dev/issues/sql-injection.md)
- [ ] User and model text is rendered as text, or sanitized with a maintained allowlist. No homemade HTML filter. (Cross-site scripting (XSS): https://shipsecurely.dev/issues/xss.md)
- [ ] Tools the model can call check the session on the server. The model does not decide who the user is. (Prompt injection in LLM features: https://shipsecurely.dev/issues/prompt-injection.md)
- [ ] Uploads have a server-side size and type limit, a generated filename, and are not served as HTML or SVG from your origin. (Unsafe file uploads: https://shipsecurely.dev/issues/unsafe-file-uploads.md)
- [ ] Cookie-authenticated APIs do not reflect arbitrary Origin headers. There is an allowlist, or no CORS middleware at all. (CORS wide open: https://shipsecurely.dev/issues/cors-wide-open.md)
- [ ] Responses send nosniff, a referrer policy, and frame protection. HSTS is only set if the site is HTTPS-only. (Missing security headers: https://shipsecurely.dev/issues/missing-security-headers.md)

## 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. (Weak password reset: https://shipsecurely.dev/issues/weak-password-reset.md)
- [ ] The lockfile is committed, and you have read the current dependency audit instead of force-upgrading. (Outdated dependencies: https://shipsecurely.dev/issues/outdated-dependencies.md)
- [ ] Privacy and terms pages exist, name the processors you actually use, and point at a contact address that reaches you. (Missing privacy policy and terms: https://shipsecurely.dev/issues/missing-privacy-policy-and-terms.md)

## Agent prompt

```text
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.
```
