We read a lot of AI-built apps. Different founders, different tools — Lovable, Bolt, Cursor, Replit, v0 — and almost always the same short list of things gone wrong. Not exotic bugs. The boring, invisible ones that pass every test, demo perfectly, and then break the moment real users and real data show up.
Here’s the complete list: the ten that show up most, what each one actually costs you, and a two-minute way to check your own app. Bookmark it as a pre-launch pass.
The 10 at a glance
| # | What goes wrong | Category | What it costs you |
|---|---|---|---|
| 1 | Secrets & API keys in the client | Security | Anyone reads your keys → charges, full data access |
| 2 | Database wide open (RLS off) | Security | Any user reads & writes every row |
| 3 | Auth enforced only in the UI | Security | Type the URL → you’re in |
| 4 | One tenant can read another’s data | Security | The leak that becomes a headline |
| 5 | No input validation | Security | SQL injection, XSS, corrupted data |
| 6 | No rate limiting | Security / Cost | Brute force + four-figure overnight bills |
| 7 | It doesn’t scale | Performance | Fine at 10 users, dead at 10,000 — on launch day |
| 8 | Silent failures & leaky errors | Ops | You learn from angry users; stack traces hand attackers a map |
| 9 | No backups | Data | One bad migration from losing everything |
| 10 | You can’t maintain it | Maintainability | A black box you (and the AI) can’t safely change |
The thread running through all ten: the app works. Every checkmark is green. That’s exactly why these survive to launch — they’re invisible until someone who isn’t you interacts with the app. Run the free 2-minute self-check for a quick read, or work through them one by one below.
1. Secrets and API keys in the client
A STRIPE_SECRET_KEY, a database service-role key, or an admin token ends up bundled into the client-side JavaScript. Why it happens: when an agent writes an API call, it inlines the value to make the example work — and six commits later it’s in your git history forever, even after you “move it to an env var.”
The cost: anyone opens DevTools, reads the key, and can charge cards, read your whole database, or impersonate your backend.
How to check: open your deployed site, view the source/bundle, and grep it for sk_, service_role, and your key prefixes. In Next.js, anything private must not start with NEXT_PUBLIC_. If you find a key, it’s already public — rotate it. → How to find the keys your app is leaking
2. The database wide open (row-level security off)
Especially on Supabase and Firebase: row-level security is disabled, or a rule was set to true “just to get it working” and never tightened. Your public anon key — which is meant to be public — then becomes a key to every row in every table.
The cost: any logged-in user (or anyone with your anon key) reads, and often writes, everyone’s data. It works perfectly for you, because you only ever look at your own.
How to check: in Supabase, confirm RLS is on for every table and each policy scopes rows to the current user. In Firebase, read your rules assuming an attacker has your config — because they do. → Supabase RLS · Firebase test mode
3. Auth enforced only in the UI
There’s a login screen, so it feels secure. But the /admin page or the API behind it has no server-side check — the UI just hides the link. Hiding a button is not access control.
The cost: anyone who types the URL or calls the endpoint directly is in.
How to check: log out (or open an incognito window) and paste your most sensitive URLs and API endpoints. If you get data instead of a redirect or a 401, the guard is missing. Every protected route needs the check enforced on the server, on every request. → Your /admin page isn’t actually protected
4. One tenant can read another’s data
If your app has teams, workspaces or organisations, the dangerous bug is a query that trusts an org_id sent by the browser. Swap the id in the request and you’re reading another customer’s invoices, orders or messages.
The cost: this is the one that turns into a headline — and a lawsuit. A generic scanner almost never finds it, because the code looks completely normal; the flaw is trusting input you shouldn’t.
How to check: for every query that returns tenant-scoped data, confirm the tenant boundary is enforced server-side from the authenticated session — never from a parameter the client can change. → The bug that lets one customer read another’s data
5. No input validation
AI loves to concatenate user input straight into a SQL query (injection) and render it without sanitising (cross-site scripting). The rule it skips: anything a user can type, upload or submit should be treated as if it’s trying to break your app.
The cost: SQL injection (read or wipe your database), XSS (hijack other users’ sessions), and corrupted or malformed data.
How to check: validate every input on the server, use parameterised queries (never string-concatenated SQL), and escape output before rendering it. The boring fundamentals the prompt never mentioned — they’re in the pre-launch checklist.
6. No rate limiting
Login, signup, password reset, “forgot password”, and any endpoint that sends email or costs you money (hello, LLM calls) usually ship with zero rate limiting.
The cost: brute-force and credential-stuffing attacks, and a bored script running your cloud or AI bill into four figures overnight — on your card.
How to check: put a basic per-IP and per-account limiter in front of auth and any expensive or outbound-email endpoint. Even a simple limit stops the cheap attacks that hit new apps first.
7. It doesn’t scale
N+1 queries, missing indexes, no caching, blocking operations — instant on ten rows, and dead at ten thousand. The AI optimises for working on your test data, not for real load.
The cost: it falls over on launch day, which is exactly when everyone arrives at once and everyone’s watching.
How to check: look for queries running inside loops, add indexes on the columns you filter and join on, and cache the reads that don’t change often.
8. Silent failures and leaky errors
Two sides of the same coin. Either errors are swallowed silently — so you find out a payment failed from an angry email days later — or raw stack traces are returned to the browser, leaking your table names, file paths, query structure and library versions.
The cost: you can’t tell when something breaks, and when it does, you’ve handed an attacker a free map of your system.
How to check: make failures visible somewhere you’ll actually see them, and never return a raw error to the client — show a generic message, log the detail server-side.
9. No backups
No automated backups, and — just as common — backups that exist but have never been restore-tested. One bad migration, one fat-fingered DELETE, one dropped table, and the data is gone.
The cost: you’re always one bad migration away from losing everything, including your customers’ data.
How to check: confirm automated backups are running and actually restore one to a scratch database. A backup you’ve never restored is a hope, not a backup.
10. You can’t maintain it
The slow one. No tests, no CI, and a codebase so tangled the AI that built it can no longer safely change it — the project has grown past what the model can hold in its head at once. You own a working app you don’t understand and can’t extend.
The cost: a black box you paid for; an expensive, slow developer handoff later, because someone has to reverse-engineer it first.
How to check: can you (or any developer you’d hire) make a change without something unrelated breaking? If the honest answer is no, the structure needs a read before it compounds.
The pattern behind all ten
Notice what they share: the app works. Every test passes, the dashboard is green, the demo is clean. None of these ten show up in how the code looks — they show up when a real person who isn’t you interacts with it, or when load, an attacker, or time arrives. That’s the gap automated checks can’t close: a scanner pattern-matches; it doesn’t know that this route is the admin route or that this query crosses a tenant boundary. A person who reads your actual code does.
If you ran down this list and more than one made you unsure, that’s the signal to have the code read before you launch. That’s exactly what a production-readiness audit is: a senior engineer reads your actual code, finds which of these ten apply to you, and tells you — in plain English — what to fix first. A fast read of the biggest launch risks, not a 200-item dump. Your green checkmarks won’t catch these. A human will.