AI-built apps break first at authorisation, database access rules, leaked secrets and unverified payment webhooks, not at the feature they were built to demo. They fail at the thing nobody demonstrated: the second user, the declined card, the request that arrives out of order.
The failures are not random. Across the AI-generated codebases we read, the same cluster recurs, and it clusters for a reason worth understanding before you read the list.
Key takeaways
- The most common serious bug in an AI-built app is authorisation: the code confirms you are logged in, then hands you a record without checking that the record is yours.
- Hosted databases like Supabase and Firebase are frequently generated with row-level security switched off and never tightened, leaving every row readable by any authenticated user.
- Secret keys read into browser-side code are public the moment the page loads, and the usual tell is a genuinely private value stored under a name prefixed for public use.
- Payment webhook handlers that never verify the provider's signature will accept an invented "payment succeeded" message from anyone who learns the URL.
- The root cause is that code generation tools produce plausible local structure but do not hold a security model across a whole application, so one system-wide rule gets rebuilt slightly differently in every file.
- A non-technical founder can confirm several of these in under an hour, using two test accounts, a database dashboard and the page source of their own live site, without reading any code.
The root cause: local plausibility, no global model
Code generation tools are very good at producing code that looks right in the file you are looking at. Ask for a page showing a user's invoices and you get one, with sensible names, a loading state and error styling. Judged locally, it is fine work.
Security is not a local property. "Only the owner of this record can see it" has to hold across every route, every query, every background job and every endpoint, including the ones written three weeks apart in different sessions. A generator that reasons a file at a time has nowhere to keep that rule, so it reconstructs an approximation each time, and the approximations drift.
That is the whole pattern. Everything below is one system-wide rule rebuilt locally, slightly differently, over and over.
Logged in is treated as authorised
This is the most common serious bug we find. The code checks that you have a valid session, then fetches the record you asked for. It never checks that the record belongs to you.
The shape is simple. A URL like `/invoices/1042` takes an ID from the request and looks it up. Authentication passed, so the request proceeds. Change 1042 to 1043 and you are reading someone else's invoice. The class of bug is called IDOR, insecure direct object reference, and it is why "we have login, so we're secure" is not a sentence that means anything.
Check it yourself. Create two accounts with different email addresses. In account A, open a record and copy the full URL including the number at the end. In a private window, log in as account B and paste that URL. You should get an error or an empty page. If you see account A's data, you have found it. Repeat for three record types: invoices, uploaded files, messages. Each was likely written by a different prompt, so one can be safe and another not.
Row-level security scaffolded open and never tightened
If your app uses Supabase, Firebase or a similar hosted database, the database itself can enforce who reads what. Getting started is easier with those rules switched off, so that is frequently how they are generated, with a comment promising to tighten them later.
Later does not arrive. The app ships with rules that let any authenticated user, sometimes any anonymous visitor holding the public API key, read every row in a table. Because the app's own screens only ever ask for the right rows, nothing looks wrong from the outside.
Check it yourself. Open your database dashboard and look at the table list. Supabase marks tables without row-level security as "Unrestricted"; Firebase has a rules tab that shows your published rules and warns about permissive ones. Any unrestricted table holding customer data, payments or messages is a live exposure, not a to-do. "The app layer handles it" is the answer that comes just before the incident.
Secrets that reached the browser
Anything shipped to a browser is public. Not "hard to find". Public. Every visitor already has a copy.
The mistake is a database service key, a payment secret key or an AI provider key being read into code that runs client-side, usually because the call was made from a page component instead of a server route. In frameworks that separate public from private environment variables, the tell is a genuinely secret value stored under a name prefixed for public use, anything beginning `NEXT_PUBLIC_` or `VITE_`.
Check it yourself. Open your live site, view the page source, and search the loaded scripts for `sk_`, `service_role`, `secret` and `api_key`. Then ask whoever built it to search the repository for `NEXT_PUBLIC` and read out what each holds. A publishable key is fine. A secret key is a rotate-it-today problem, and rotating is not optional, because you cannot know who already copied it.
Payment webhooks accepted without verification
When Stripe or a similar provider tells your app a payment succeeded, it sends a signed message. The signature is what makes the message trustworthy. Generated handlers often parse the body, read the amount and the customer, mark the order paid, and never verify the signature at all.
The consequence is direct: anyone who learns your webhook URL can send an invented "payment succeeded" message and receive whatever your app grants paying customers. Handlers also tend to assume each event arrives once, when providers retry deliberately, which produces duplicate credits and emails.
Check it yourself. Ask one question and listen to the exact answer: "show me the line where we verify the webhook signature." For Stripe that is a call to `constructEvent` with a signing secret. "We check the event looks right" is a no. Then ask what happens if the same event arrives twice, and have them show you the code rather than describe it.
The same rule, implemented three times, drifting
Generated code duplicates. The trial length, the discount calculation, the definition of an active subscription each get written fresh wherever they are needed, and every copy is correct the day it is written.
Then a rule changes, one copy gets updated, and the others quietly disagree. The billing page says one thing, the receipt another, the admin panel a third, and nobody can say which is the truth.
Check it yourself. Pick a rule you can change from an admin screen. Change it once. Then walk every surface that should reflect it: the signup screen, the pricing page, the confirmation email, the invoice, the admin view, the exported report. Anything still showing the old value is a second copy of the rule you did not know existed.
No error handling off the happy path
The demo worked. The demo is one path through the software, with valid inputs and a working network.
Things that reliably break: a form submitted twice because the button did not disable, a payment declined rather than approved, an oversized upload, a session that expired overnight, a name with an apostrophe. The failure mode is rarely a crash. It is a blank screen, a spinner that never stops, or worse, a success message for something that did not happen.
Check it yourself, in about twenty minutes, on a staging copy:
- Double-click every submit button, then check whether you created two records.
- Use your payment provider's test card for a decline and read what the user is told.
- Turn off wifi halfway through a save, turn it back on, see what the app now believes.
- Leave a logged-in tab open overnight, come back and click save.
- Put an emoji and an apostrophe in every free-text field.
- Upload a file ten times larger than anything you expected.
Write down every moment the screen goes blank or lies to you. That list is a bug backlog you produced without reading a line of code.
No tests, so nobody knows when something breaks
The others are bugs. This one is why bugs stay.
Without tests, every change is a gamble that someone will notice the damage. In an AI-assisted codebase that compounds, because the tools rewrite code they were not asked to touch and nothing signals when they do. Teams end up afraid of their own software, which is how a six-month-old product becomes one everybody would rather rebuild.
Check it yourself. Ask for the single command that runs the test suite, how long it takes, and to watch it run. Then ask what would have caught the last bug that reached a customer. A pause is the answer.
What this costs to find, and when not to pay us
Every check above is free. Do them first. A founder who has run the two-account test and read their own database rules is having a different conversation with their engineers within the hour.
Some problems stay invisible from the outside: the background job that runs as an administrator, the cache that serves one customer's page to another, the migration that will fail on production data volumes. Our Tech Audit is $4,999 and covers exactly the things the checks above cannot reach, scoped to one primary codebase and its deployment configuration. What should land in your inbox at the end of it is set out in what belongs in a code audit report.
That is not the right spend for everyone. If you want a written second opinion rather than a full review, cheaper fixed-price options exist and are honest about their scope: Afterbuild Labs publishes a $49 Async Audit and a $499 Security Audit (afterbuildlabs.com, Aug 2026), GreeLogix starts from $500 with 5-day delivery (greelogix.com, Aug 2026), and Enviera starts from $950 on a 72-hour turnaround (enviera.com, Aug 2026). We compare the field, including where others are the better buy, on our fixed-price code audit comparison.
None of this means the AI-built version was a mistake. Getting to a working product quickly is worth a great deal. It means the version that won you customers and the version that can safely hold their data are not the same version, and the gap is smaller and more specific than the panic suggests.
Questions founders ask
Is my AI-built app actually insecure, or is this scaremongering?
Run the two-account test above. It takes ten minutes and gives you a definite answer about your app rather than a general claim about apps. If account B can read account A's records, the question is settled.
Can I fix these myself if I can't code?
Not directly, but you can find them, describe them precisely and verify the fix. "Account B can open account A's invoice at this URL" beats a vague worry, and re-running the test proves the fix landed.
How long does fixing this cluster take?
Mostly it depends on how much duplication is in the codebase. Authorisation checks and database rules are usually days rather than weeks, because the fix is one pattern applied consistently. Duplicated business rules take longer, since every copy has to be found before it can be removed.
Should I just rebuild instead?
Usually not on the strength of these failures alone. They are repairable in the existing code. Rebuilds are justified by a data model that cannot represent what the business now does, not by missing security checks. If you are raising in the next six months, fix these before diligence rather than during it. The founder-side checklist is here.
What does a code audit cost?
Published fixed prices run from under a hundred dollars for an async written opinion to five figures for diligence-grade reviews. We break down the tiers in how much a code audit costs.