Why Lovable Apps Need a Code Audit: Two Real Cases
Two real cases from vibecoded apps — and why a code audit isn't distrust toward Lovable, but the step that turns a prototype into a product.
I build frontends with Lovable myself. For a clickable prototype, a landing page, or a first end-to-end slice, it's fantastic: within hours you have something you can touch, show, and test. That's not a marketing promise, it's everyday reality.
And this is exactly where the misunderstanding starts. "Looks like it works" and "is built to last" are two different statements. The difference almost always lives in the architecture — and you can't see architecture in a running demo.
A demo runs on the happy path: a handful of records, one friendly user, no load, no attacker. What happens under load, with real data, or under a security review is something nobody clicks through. That's why the expensive mistakes stay invisible — until they aren't.
Two cases from the field.
Case 1: The job board that filters in the browser
A job board, clean-looking, with filters for city, salary, and industry. Works flawlessly in the demo. In the code it turns out: the filtering happens in the client.
ts
// Load all jobs, then filter in the browser
const { data: jobs } = await supabase.from('jobs').select('*')
const visible = jobs.filter(
(j) => j.city === city && j.salary >= minSalary
)That's the fastest path to a working demo — and in production it's a problem twice over.
Security. The browser receives all the records before any filtering happens. Open the dev tools and you see every listing in the network response — including drafts, deactivated postings, or entries that shouldn't be visible at all. A filter in the client is never a security boundary. It's cosmetics.
Scalability. With 30 jobs, nobody notices. With 30,000, every page load pulls the entire dataset onto the device. Slow, expensive, unusable on a phone.
Done right, filtering belongs where data authority lives:
ts
// Filter in the query — the server only returns what's permitted and needed
const { data: jobs } = await supabase
.from('jobs')
.select('*')
.eq('city', city)
.gte('salary', minSalary)Plus Row Level Security, pagination, and indexes. None of this is exotic — but none of it appears on its own when the goal is "runs in the demo."
Case 2: The ERP that supposedly does deep learning
A domain expert specs a feature for an ERP tool that's supposed to be solved with deep learning — say, a demand forecast. The result looks coherent: plausible numbers come out, the UI talks about an "AI-powered forecast." But look into the code and there's no model. There are if-then-else rules.
ts
// Sold as an "AI-powered demand forecast"
function forecast(history, month, weekday) {
if (month === 12) return base * 1.5 // holiday season
if (weekday === 6) return base * 0.7 // Saturday
return base
}No training, no model, no learning from data. It works on exactly the examples it was built with — and turns brittle everywhere else.
The reason is structural: the tool sticks to its stack. Lovable generates excellent TypeScript, React, and Supabase. A real ML pipeline — data preparation, training, model, inference — lies outside what it can ship. Instead of saying "this can't be done that way," it produces the next best thing expressible within its own stack: hardcoded heuristics dressed up as AI.
An LLM code generator rarely says "I can't." It delivers the plausible-looking surrogate — and that's the more dangerous failure, because nobody notices it.
The stakeholders make decisions assuming they have a learning system. In reality they have a handful of rules that no one touches again.
The common pattern
Both cases look like the solution. Neither is. And in both, the gap is invisible without expertise:
The tool optimizes for what's convincing in the demo — not for what holds up under load, with real data, or under a security review.
The tool optimizes for what's expressible within its stack — and silently substitutes real requirements with the nearest surrogate.
This isn't an argument against Lovable. It's a property of the tool class. Those who know it use it well: for speed at the start — and with a checking eye before things get serious.
What a code audit actually checks
No distrust, just a few concrete questions:
Where does filtering and authorization happen? In the client or on the server? Does Row Level Security actually bite, or is it merely configured?
What's behind the promises? "AI," "real-time," "search" — is there substance underneath, or a placeholder that just looks good in the demo?
What happens at 100x the data? Does the architecture scale, or does it break on the first real load?
Where does the data live, and who sees what? Does anything leave the backend that shouldn't?
These questions cost a few hours. Not asking them costs a migration, a security incident, or a product built on a lie.
Vibe coding is an excellent starting point. It's just not an endpoint. The step from "works in the demo" to "holds up in production" is exactly the step an audit makes visible — before reality takes care of it for you.