The Hidden Debt in AI-Generated Code
The demo worked. Now what?
A founder builds an MVP in Lovable over a weekend. It looks great, investors nod, the first users sign up. Or a small team leans hard on Cursor and ships six months of roadmap in six weeks. Or a freelancer delivers the app on time and under budget, and only later does anyone wonder how.
We audit a lot of these codebases now. To be clear up front: this is not an anti-AI article. The tools are genuinely good at producing working software fast, and “we validated the idea for $200 of tokens” is a rational trade. The problem is narrower and more specific: AI-generated code optimizes for the demo, and the gap between “demos correctly” and “operates safely” is exactly where the debt hides. The model was rewarded for code that runs, not code that survives contact with hostile users, growth, or month twelve.
The failure modes are remarkably consistent across tools: Lovable, Bolt, v0, Replit, Cursor-heavy teams, AI-assisted freelancers. Here’s what we keep finding.
The recurring failure modes
1. Secrets in the client, secrets in the repo
The classic. An API key pasted into a React component “to make it work,” which means it ships in the JavaScript bundle to every visitor. Or a .env committed because nobody added it to .gitignore and the AI didn’t insist. We’ve seen OpenAI keys, Stripe secret keys, and service-role database tokens sitting in client bundles, the service-role token being the fun one, because it typically bypasses every access rule in the database.
Anyone can open DevTools. Assume someone has.
2. Authorization that only exists in the UI
AI tools are good at authentication: the login flow usually works, because it’s the most-documented pattern on the internet. Authorization is where it falls apart. The generated pattern is typically: hide the admin button if the user isn’t an admin. The API route behind the button checks nothing.
On Supabase-backed apps (most of the Lovable/Bolt output we see), this shows up as missing or permissive Row Level Security: the client queries the database directly, and nothing server-side stops user A from requesting user B’s rows:
-- What we find: RLS enabled, policy wide open
create policy "allow all" on invoices for select using (true);
-- What it needed
create policy "own invoices" on invoices
for select using (auth.uid() = user_id);One changed line between “multi-tenant SaaS” and “public dataset.” This class of flaw (broken object-level authorization) tops our findings in AI-built apps, and it’s the same family of issue we cover in top security vulnerabilities code audits catch.
3. No error handling, only happy path
The generated code assumes the network is up, the API responds, the JSON parses, and the user typed sensible things. Every await is naked. When something fails, the failure is silent or the app white-screens, and because there’s also no logging or error reporting (Sentry is never in the generated stack unless you asked), you find out from a customer email. Three weeks later.
4. Unbounded queries and N+1 by default
select * with no pagination, no limit. Fine with 50 demo rows; a timeout with 80,000 real ones. Loops that fire one query per item because the model composed “get list” and “get details” snippets without noticing the multiplication. These are ordinary performance bottlenecks; the AI wrinkle is that they’re everywhere at once, because no single author ever held the data model in their head.
5. Zero tests, and nobody who can tell
Vibe-coded apps almost never ship with tests; the demo doesn’t need them. That’s survivable early. The compounding problem: when each subsequent feature is also AI-generated against an untested codebase, regressions land silently, and the humans involved may not read the code closely enough to catch them in review. The codebase grows while the number of people who understand it stays at zero.
6. Duplication instead of design
Ask an AI for a feature five times and you get five parallel implementations: five date formatters, three fetch wrappers, two subtly different permission checks. Models generate locally; they don’t refactor globally unless driven. The result is a codebase where fixing a bug means finding all N copies of the logic and hoping you got them all. It’s the same disease catalogued in code smells every audit flags, at generation speed.
7. Dependency sprawl and hallucinated packages
Every generated snippet may pull a new library, so you end up with three HTTP clients and four UI kits, none updated since generation day. The sharper edge: models sometimes cite packages that don’t exist, and attackers have taken to registering those names with malicious code (“slopsquatting”). If nobody audits package.json, nobody knows what’s actually in the supply chain.
Why this debt is hidden, specifically
Hand-written bad code usually looks bad: inconsistent, commented with apologies, visibly rushed. AI-generated code looks clean. Consistent formatting, plausible naming, confident structure. Every surface signal a reviewer uses to gauge quality reads “senior engineer,” while the substance underneath may be demo-grade.
This breaks the normal correlation between how code looks and how it behaves, which is precisely why automated scanning under-detects it and skimming reviews miss it. A linter will pass most of the code above. The problems are semantic: the RLS policy that exists but permits everything, the auth check on the button but not the endpoint. Finding those requires a human asking “what happens if a hostile user calls this directly?” (the core argument of manual vs. automated code audits).
If you shipped AI-generated code, in order
- Scan for secrets today: repo history and client bundle both (
gitleaksortrufflehogfor the repo; grep your built JS forsk_live,service_role,AKIA). Rotate anything found. Rotation, not deletion; the key is already public. - Test authorization from outside the UI. Call your API as user A requesting user B’s data, with curl, not the app. If you’re on Supabase, review every RLS policy.
- Add error reporting (Sentry or similar) so failures become visible. You can’t fix what you can’t see.
- Cap your queries. Pagination and limits on every list endpoint.
- Write characterization tests for money and auth paths: not full coverage, just the paths where a silent regression costs real money or real data.
- Then triage the rest like any technical debt; the framework in Technical Debt Explained applies unchanged. AI debt is still just debt; there’s simply more of it per calendar month.
Get a free code audit
If your product was built with Lovable, Bolt, v0, Replit, Cursor, or a freelancer whose velocity seemed too good to be true, the honest answer is you don’t know what’s in there until someone reads it. We offer a genuinely free manual code audit: a Webisoft engineer reads the actual code, runs exactly the checks above, and sends you a written report of what’s solid and what’s a problem. It’s free, it’s done by a human, and there’s no obligation attached to the findings. Get a free code audit before your users audit it for you.