A founder handed us a Lovable-generated app last month and asked the simple question every founder asks: "Can you scale this?" The product had paying users, a working onboarding, and a marketing page that looked sharper than most Series A sites. We agreed to look before we quoted.
We ran a 30-item audit before we touched a single file. In this repo, by lunch, 27 of those items had failed. None of them were cosmetic.
This post is the audit, in the order we ran it, with the queries and commands we used. It is not an argument against Lovable, Bolt, v0, Cursor, or Replit Agent. Those tools are real, and they ship usable day-one code. The argument is narrower: vibe-coded output is day-one code, not production code. If you take it to real users without a senior pass, the failure modes are predictable and they are mostly invisible from the outside.
What we expected, and what fell out
Imagine you're a founder who shipped your MVP in a weekend on Lovable, picked up 4,000 signups in a quarter, and now you want to add billing, a second user role, and a mobile app. You ask an agency to scale it. You expect feedback like "the components could be tidier" or "you'll want to extract the auth context." You don't expect the report we sent back.
Here is what showed up in the first hour, before we read a line of business logic:
- With the project's current RLS and grants, the Supabase anon key in the browser bundle could read every user's records.
- Two row-level security policies were the wrong shape and one was inverted.
- The schema referenced a table that did not exist in the live database.
- One list view, after the Supabase API row cap had been raised, fired 50,001 queries to render 50,000 rows.
- The repo had zero tests and no CI gate.
package.jsoncarried 110 dependencies; the app actually used about 30.
We have seen versions of this before, and the public scans point in the same direction if you keep the source quality straight. Matt Palmer's CVE disclosure describes Lovable-generated projects with insufficient Supabase RLS; Vibe App Scanner's summary reported 170 exposed projects and 303 vulnerable endpoints, while NVD marks CVE-2025-48757 as supplier-disputed. Escape's methodology says its team scanned 5,600 public vibe-coded apps and found 2,000+ high-impact vulnerabilities and 400+ exposed secrets, with sampling and passive-scanning limitations. CodeRabbit's 470-PR report found AI-co-authored PRs carried about 1.7x more issues overall and up to 2.74x more security issues.
None of those numbers proves every vibe-coded app is broken. They prove the same invisible failure modes keep recurring, and this is what they look like inside one repo.
The audit, run in order
Five categories, in the sequence we work through them. The first one is where most of the damage lives.
1. Secrets and row-level security
Open the deployed app. DevTools → Network → filter on /auth/v1 or /rest/v1. The request headers tell you which Supabase key the client is carrying. If apikey decodes to "role":"service_role", the audit is over and you rotate immediately.
In this codebase it was the legacy anon key: the browser-exposed key Supabase expects for public clients. That is fine, by design, if grants and RLS are correct.
It was not. From a psql shell against the project's database:
select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public';
Two tables had rowsecurity = false. For the rest, we listed policies and found the canonical Lovable anti-pattern:
CREATE POLICY "Users can view data"
ON public.user_data
FOR SELECT
USING (auth.role() = 'authenticated');
That policy reads as: any logged-in user can read every row in this table. Sign up a free account, get the anon key from your own bundle, and you can read every other customer's data. The correct shape is USING (auth.uid() = user_id). Supabase's model is simple: the API key says what application is calling, Auth says who the user is, and RLS decides which rows that user can touch.
We confirmed it with a curl against the public REST endpoint, using the anon key from the bundle and a fresh user's JWT:
curl "$URL/rest/v1/user_data?select=*" \
-H "apikey: $ANON" \
-H "Authorization: Bearer $JWT"
It returned every row. We also found a second table where the write policy was the wrong shape:
CREATE POLICY "Users can add rows"
ON public.user_data
FOR INSERT
TO authenticated
WITH CHECK (auth.role() = 'authenticated');
That policy does not check ownership. Any logged-in user could insert a row with someone else's user_id. In Postgres RLS, a missing INSERT policy denies by default; broad WITH CHECK policies are what make writes dangerous. If this fails, your next feature sprint is paused until private customer data is private again.
That is the Moltbook pattern: the public Supabase key was not the bug by itself. Missing RLS made it powerful enough to expose 1.5 million API tokens and 35,000 emails.
The Lovable security scanner had given the project a green check, because, as the Vibe App Scanner write-up puts it: the scanner checks whether RLS is enabled, not whether the policies match the product's authorization model. A green check is not a substitute for reading the policies.
2. The schema was partly hallucinated
Once we trusted the auth model less, we trusted the schema less. We diffed the table names referenced in the TypeScript against the live information_schema.tables. Three tables in the code did not exist. One: users_profile, had been in a draft migration that was never run; the LSP types still claimed it existed because the generated database.types.ts was stale.
Two foreign keys had no indexes. Postgres does not auto-index foreign-key columns; you add those indexes yourself. The result was a JOIN between orders and customers doing a sequential scan on 80,000 rows every time a list view loaded.
We also found three columns where a JSON blob was carrying what should have been a relational table: metadata jsonb with a stable shape, queried with ->>'customer_id' in three different files, never indexed. Inigra's self-published audit of roughly 600,000 lines of vibe-coded output puts this near the top of the recurring patterns: AI is consistently good at the visible (UI, features, flows) and consistently weak at the invisible (security, testing, architecture).
3. The architecture had collapsed under itself
App.tsx was 612 lines. It held the router, the auth context, the Supabase client, three useEffect blocks for hydration, and the dashboard's data fetching. The model had kept appending because that is where the cursor was. There were three slightly-different copies of the same form-validation function in three components. GitClear's 211M-line AI code quality report found copy/pasted lines rose to 12.3% of changed lines in 2024, up from 8.4% in 2021, and this repo was worse than that benchmark.
The list view that loaded the 80,000-row table fetched it like this:
const orders = await supabase.from('orders').select('*')
const enriched = await Promise.all(
orders.data!.map(async (o) => {
const customer = await supabase
.from('customers')
.select('*')
.eq('id', o.customer_id)
.single()
return { ...o, customer: customer.data }
})
)
In this project, Supabase's default 1,000-row API cap had been raised, so the list path really did fetch 50,000 orders. That meant one query for the list and 50,000 queries inside the .map(), 50,001 total per render. On the default cap, the same code still fails; it fails as 1,001 requests before it fails as 50,001.
The fix is a single .select('*, customers(*)') with a foreign-key index on customer_id. The N+1 is the obvious problem; the lack of pagination is the worse one. There was no .range() or .limit() anywhere in the file.
There were no error boundaries. One unhandled promise rejection blanked the whole app. Two useEffect blocks shared state in a way that re-triggered each other on every render. We ran tsc --noEmit --strict and stopped counting at 180 errors.
4. Dependencies were carrying weight, and CVEs
npm ls --depth=0 returned 110 packages. We grepped imports and the app actually used around 30. The drift was the Lovable signature: lucide-react, react-icons, and heroicons all installed because three different prompts had each picked their favorite. date-fns and dayjs both present. Zustand and Redux Toolkit both wired into the app, holding overlapping state.
npm audit --omit=dev returned eleven advisories, four of them high: an outdated axios, an old next-auth, a vulnerable serialize-javascript transitive. There was no package-lock.json committed, so every CI run (when there was a CI run) would have resolved a different transitive tree. Nothing in the build pipeline would have caught a typosquatted package, and AI tools are already part of the supply-chain blast radius, in the 2025 Nx incident, Snyk reported that malicious packages used local AI-agent reconnaissance and exfiltrated GitHub tokens, npm tokens, SSH keys, and environment variables.
5. Tests and observability did not exist
find . -name "*.test.*" -o -name "*.spec.*" | wc -l
Zero. The only file in tests/ was the Vitest example template. Coverage tooling had never been wired up.
There was no CI workflow at all, pushes to main deployed straight to Vercel. No build gate, no test gate, no lint gate, no type-check gate.
Sentry was not configured. There was no structured logging, no health-check endpoint, no error tracking of any kind. The first time a user hit a 500, the founder would learn about it from a Twitter DM.
This matches the broader direction of the developer surveys and PR studies. Harness reported that 67% of developers spend more time debugging AI-generated code, and CodeRabbit found AI-co-authored PRs carried about 1.7x more issues. When the codebase grows faster than the test suite, every regression becomes archaeology.
What we replaced, and what we kept
The conversation with the founder was short. Two options on the table:
- Patch the security holes in place, tell the team this is the floor, and re-quote in a quarter when something else breaks.
- Keep what works, throw away what doesn't, and rebuild the data layer behind the same UI.
We argued for option two and they agreed. The UI was the part of the codebase the model was good at: the components were inconsistent but they were on-brand and the founder liked them. The data layer, the auth model, and the deployment pipeline were where the rot was. Replacing those without changing the surface is a tractable three-week job. Patching them in place is an open-ended one.
What we kept:
- The Tailwind theme, the component primitives, the page layouts, the marketing site.
- The Supabase project itself, plus the existing user accounts.
- The product's information architecture and the founder's content.
What we replaced:
- Every RLS policy, written from
auth.uid() = user_idoutward, with apgTAPtest per policy that asserts a second user cannot read or write the first user's rows. - The Supabase client wrapper, with a server-only admin client behind API routes that re-verify the JWT on every request.
- The schema, three real tables in place of the JSON blobs, foreign-key indexes added, the hallucinated
users_profileeither created properly or removed from the code. - The list views, paginated with
.range()and joined with.select('*, customers(*)')instead of the N+1. package.json, down from 110 dependencies to 34, with a committedpnpm-lock.yamland Renovate set to weekly.- A GitHub Actions pipeline:
tsc --strict, ESLint, Vitest unit tests, Playwright smoke tests, and agitleaksscan, all gating the deploy. - Sentry on the front and back end, with source maps and PII filtering, plus a
/healthzendpoint and structured logs piped to Logtail.
Three weeks later we shipped. The founder kept the surface their users already knew. The new data layer carried the same product, just with the floor under it that should have been there from the start.
The lesson, and where it leaves you
Vibe-coding tools are doing something genuinely useful. The first version of a product in an afternoon is a real change in how startups can move, and we are not nostalgic for the era when you needed a four-week sprint to put a working dashboard in front of a user. The mistake is treating that first version as the production version.
The public numbers are messy; the failure modes are not. That is why the audit is fast. RLS that allows any authenticated user, browser-exposed keys mistaken for authorization, broad WITH CHECK policies, hallucinated tables, missing foreign-key indexes, no tests, no CI, three icon libraries. If you are reading this and you have shipped an MVP on Lovable or Bolt that is starting to take traffic, you can run most of this checklist on yourself in an afternoon.
If you would rather hand it to someone, that is what our engineering practice does. We also fold the AI work into the same engagement when the product needs it: the rebuild we just described kept a vibe-coded UI on top of a hardened backend, and that is the shape most of these projects need.
If you have built on Lovable or Bolt and you are ready to scale, this is the audit we start with in week one. Book a call.