The pre-launch security checklist for apps built with AI
The 15 things that break AI-built apps once real people arrive — and how to check each one before they do.
Launchprep reads your code on your machine and runs 290 checks before you launch — security, but also the legal, billing, data and UX problems that break a launch without anyone being hacked. 117 of them run free: npx launchprep. This page is the manual version of what it runs.
Whether you call it vibe coding or AI-assisted building, the failure modes are the same — and they are checkable before launch. If you built something with Cursor, Claude, Lovable, v0, Bolt or Replit and you are about to put it in front of real people, this is what to look at. 15 categories, in the order they tend to hurt. None of it assumes you are a security engineer.
One section below talks about privacy law. That is general information, not legal advice — it cannot know where your users are or what you do with their data. Treat it as a prompt to check, and check anything that matters with a lawyer in your jurisdiction.
01 Your secret keys are probably in the browser
The most common leak in AI-built apps, and the cruellest, because the name reads like it is protected. Any environment variable prefixed NEXT_PUBLIC_, VITE_ or REACT_APP_ is compiled into the JavaScript every visitor downloads. An assistant will happily add that prefix to make an error go away, the app starts working, and nobody looks again.
- Open your deployed site, view source, and search the bundle for the first six characters of each key you hold.
- Move anything secret behind an API route or a server action, so the browser asks your server and never holds the key.
- Rotate any key that was ever public. A key in a shipped bundle is a key someone already has.
- Publishable keys are fine — Stripe’s
pk_is designed to be public. Secret keys, database URLs and provider tokens are not. - Check your git history too. A key you removed last month is still in the commit that added it.
02 Your database may be open to the internet
Managed databases are reachable from anywhere by default; what stops a stranger reading your tables is a policy you have to write. Supabase and Firebase both ship this way, and the app works perfectly whether or not the policy exists — you cannot tell by using it.
- Turn on row-level security for every table holding user data, then write a policy for each. A table with RLS on and no policy denies everyone, which is the safe direction to fail.
- Check storage buckets separately. They have their own rules and default to public more often than people expect.
- Sign in as one account and try to read another account’s row. If it works for you, it works for anyone.
- Never ship the service-role key to the browser. It bypasses every policy you just wrote.
03 Being signed in is not the same as being allowed
This is the failure AI-generated code makes most reliably. The route checks that you are logged in, then trusts the id in the URL. Change /invoices/1042 to /invoices/1043 and you are reading someone else’s invoice — the login check passed, and it was never the question.
- For every route that takes an id, ask: does this confirm the record belongs to the person asking?
- Scope queries by the account as well as the id — filter on both, in the same query.
- Check write and delete paths, not only reads. They are easier to forget and worse to get wrong.
- Do not rely on the interface hiding a button. Anyone can call the endpoint directly.
04 One customer, another customer’s data
If your app has teams, organisations or workspaces, every query needs to know which one it is answering for. Miss it once and a customer sees a stranger’s numbers. List endpoints usually get scoped because somebody noticed the wrong rows; counts, exports and search frequently do not.
- Find every query that filters by an account or organisation, and then find the ones that do not.
- Push the rule into the database with row-level security rather than remembering it in each query. One forgotten
WHEREis a breach. - Check aggregates and search especially — a total that includes other tenants leaks how big they are.
- Removing someone from a team should end their session, not just hide the button.
05 Sign-in that can be walked around
Authentication is where an attacker starts, because it is the one door everyone can see. The common gaps are not exotic: no limit on password attempts, a reset link that never expires, and an error message that quietly confirms which email addresses have accounts.
- Limit login attempts per account and per address, and slow down after a handful of failures.
- Give password-reset tokens a short expiry and make them single-use.
- Return the same answer and take the same time whether or not the email exists — otherwise anyone can build a list of your users.
- Verify email addresses at sign-up, especially if any permission anywhere is decided by matching an address.
06 AI features can spend your money without limit
A model call with no token ceiling generates until the model decides to stop, and you pay for all of it. Add a public endpoint and a retry loop and the bill is no longer bounded by anything you control. People discover this from an invoice.
- Set a maximum token count on every model call. There is no sensible default.
- Put a per-user limit on any endpoint that calls a model, and require sign-in for the expensive ones.
- Cap retries. A loop that retries on failure will retry on a failure that repeats.
- Set a hard spend limit in the provider’s dashboard as the last line of defence — and check whether a per-user cap adds up to an unbounded total across all users.
07 Anything your AI reads, a stranger can write
If your app feeds user text, uploaded documents or fetched web pages into a model, someone will eventually write instructions in there instead of content. The model cannot tell your instructions from theirs. It becomes serious the moment the model can do something — send an email, call an API, change a record.
- Treat model output as untrusted input. Validate it before it reaches anything that acts.
- If the model can call tools, decide which actions need a human to confirm. Anything that spends money or sends a message usually does.
- Never put a secret in a prompt hoping the model will keep it. It will not.
- Log what the model was asked and what it did, so that when something odd happens you can see why.
08 Anything a user can type, a user can abuse
Rate limiting is what stops one person making your service unusable for everyone else, and it is almost never in a first draft. Login and password reset matter first: without a limit, a script can try passwords all night, or flood one person’s inbox.
- Limit login, password reset and sign-up, per address and per account.
- Limit anything that sends an email or a text. Those cost money and get your domain blocked.
- Limit the expensive endpoints — search, export, anything that calls a model.
- Validate on the server. A check that only runs in the browser is a suggestion.
09 Letting people upload files
An upload is a stranger putting a file on your computer. The two mistakes are trusting the name and trusting the size: invoice.pdf.php is not a PDF, and nothing stops someone sending a file until your disk is full.
- Judge the type by the actual bytes, not the extension the caller chose.
- Set a maximum size. Most upload middleware accepts anything until you tell it not to.
- Store uploads outside the web root, or in object storage, and serve them through a route that checks who is asking.
- Strip metadata from photos. A picture taken on a phone carries the place and time it was taken.
10 Taking money without leaving holes
Payment providers do the hard part, but the join between them and your database is yours. The classic mistakes are trusting an amount that came from the browser, and trusting a webhook you never verified.
- Verify the webhook signature. Without it, anyone who finds the URL can tell you they paid.
- Calculate the price on the server. Never take the amount from the request.
- Make webhook handling idempotent — providers retry, and a retry must not grant the thing twice.
- Decide what happens when a payment succeeds but your handler fails. That gap is where money and access come apart.
11 The law reaches you from the first email address you store
Privacy rules are not a large-company problem. In the EU, the UK and California they apply from your first user, and app stores reject submissions without a policy. This is also the cheapest category to fix, because most of it is writing rather than engineering.
- Publish a privacy policy saying what you collect, why, who else receives it, and how someone deletes it.
- Publish terms. They are what let you close an abusive account and cap what you owe when something breaks.
- Put a monitored address on the site. A user with a billing problem and nowhere to write disputes the charge instead.
- Name a security contact. A researcher who cannot reach you posts publicly instead.
12 What happens to the data you promised to delete
“Delete my account” is a promise with a long tail. The row goes, and the copies stay — in logs, in analytics, in the error tracker, in a table nothing cascades to. Most apps never build the deletion path at all, and then a request arrives and it is done by hand, badly.
- Build a real deletion path before you need one, and follow it through every table that references the user.
- Decide how long logs and error records live. An error row with an account id in it is personal data with no expiry.
- Know what is in your backups and how long they keep it, because that is part of the answer you owe.
- Check what your analytics and error tools retain. Deleting your copy does not delete theirs.
13 It works with ten users. That is not evidence.
Most AI-written database code fetches everything and filters afterwards. That is instant with fifty rows and a timeout with fifty thousand — and nothing in the code changed between the two. The same is true of a missing index: invisible, then suddenly fatal.
- Find the queries with no limit and no pagination. Those fall over first.
- Index the columns you filter and join on. Postgres does not index foreign keys for you.
- On a serverless host, route through a connection pooler or you will exhaust the database’s connection limit.
- Try it with a realistic amount of data before real users bring it.
14 The deploy itself
The gap between “works on my machine” and “survives a deploy” is where a good launch goes wrong at the last moment. A migration that fails halfway leaves the schema in a state nothing knows how to undo, at the worst possible time.
- Write a way back for every migration, and know which ones cannot be reversed.
- Check that a deploy does not drop requests in flight, and that background jobs finish rather than being killed.
- Keep secrets out of the build. A key in a CI log is a leaked key.
- Have backups, and restore one at least once. A backup nobody has restored is a hope.
15 If it breaks, will you find out?
The difference between a small incident and a bad one is usually how long it ran before anyone noticed. A new app typically has no error reporting at all, which means the first report comes from a user — if it comes.
- Add error tracking before launch, not after the first outage.
- Make sure errors reach you somewhere you actually read.
- Do not show the raw error to the user. Provider errors name your model, your organisation and your limits; show a reference and log the detail.
- Watch for the quiet failures too — a feature that silently stopped working looks like nothing at all.
Or have it checked for you
Everything above can be checked by hand, and the list is worth reading either way. Launchprep does the deterministic half automatically — it reads your code, works out what you actually built, and runs only the checks that can apply to it.
npx launchprep
Free, unlimited, and it runs on your machine — nothing is uploaded. See what it covers.
Which of these actually happen?
This list is the shape of the problem. For which parts of it real teams miss most often, I ran all 290 checks against 68 mature open-source projects — Django, Rails, Sentry, GitLab, Supabase — and counted: the production mistakes that show up in almost every codebase.
Know one this list is missing?
This list is short because it is the shape of the problem, not the whole of it. If you have seen a launch break in a way it does not cover, add it — what is not already covered becomes a check every builder gets.
Run every check that applies to your project, on your machine, free and unlimited:
npx launchprep