On this page 7 sections
Vibe-coded MVPs built with Lovable and Bolt break in production in five predictable places: auth that lets users read each other’s data, error handling that fails silently, N+1 queries, missing observability, and schemas built only for today. Auth is the worst - 170 of 1,645 scanned Lovable apps exposed data. Fix it first, before any more users.
Cursor apps hit most of the same problems. We’ve run Code Rescue engagements on MVPs from all three, and the same five seams split in roughly the same order. The apps that survive are the ones where the founder catches them early.
Auth that looks right but isn’t
Vibe-coded apps almost always have auth in the sense that users can sign up and log in. What they’re missing is the layer that stops User A from reading User B’s data.
In Lovable apps built on Supabase, the root cause is tables exposed through the Supabase API without Row Level Security enabled, or with RLS policies that don’t match the app’s logic. The public anon_key is embedded in the client bundle - that’s expected, it’s how Supabase is designed. What makes it dangerous is when there are no RLS policies restricting what that key can access. Without them, anyone with browser devtools can query every row in every table: user profiles, payment records, private messages, whatever the app stores. No authentication required.
CVE-2025-48757, scored CVSS 8.26 in the researchers’ disclosure, documented this at scale. Of 1,645 Lovable projects analyzed, 170 (about 10.3%) had inadequate RLS, across 303 endpoints reachable without logging in. The same mistake hit outside Lovable too: Moltbook, a vibe-coded app on Supabase with no RLS, exposed 1.5 million API authentication tokens and 35,000 email addresses before Wiz researchers reported it.
Beyond RLS, there’s a second auth failure in most vibe-coded apps: trusting client-passed IDs for object ownership. A route like GET /api/invoices/:id that queries the database by the ID in req.params without checking whether the logged-in user owns that invoice is a broken object-level access control bug. Any authenticated user can read any record by guessing or iterating IDs.
The fix. Add RLS policies to every table - user_id = auth.uid() at minimum - before you take your first real user. For ownership checks: verify ownership server-side on every route that reads sensitive records, regardless of what the client sends.
Error handling that leaves your users guessing
Vibe-coded apps handle the happy path. The sad path - an external API returns 503, a Stripe webhook arrives with a malformed payload, a database query times out at peak load - usually triggers one of two outcomes: a silent failure that no one notices, or an unhandled exception that crashes the flow and surfaces a raw stack trace.
Silent failures are the more dangerous of the two. A payment confirmation that never confirms. An email notification that never sends. A webhook that gets dropped because the payload didn’t match what was expected. The user sees nothing. You see nothing. The problem piles up until someone complains - and by then, it may have compounded across several releases.
The CodeRabbit December 2025 analysis of 470 open-source pull requests found 10.83 issues per AI-co-authored PR versus 6.45 for human-only PRs. Error handling and exception-path gaps were nearly 2x more common in the AI-co-authored code. AI tools generate code optimized for ideal conditions - clean inputs, responsive APIs, no network blips. Production is none of those things.
The fix. Every call to an external service needs a try/catch. Every webhook handler needs input validation before any processing. Show users a message that says something went wrong, and keep the underlying error in your logs. Add retry with exponential backoff for transient failures (rate limits, network blips) so they don’t fail immediately and silently.
Missing observability - flying blind when it breaks
Most vibe-coded apps have exactly one window into production: whatever the developer console logs. When something breaks for a real user, you find out from user reports or from noticing the app is down.
This is a solved problem that most founders defer until after their first bad incident. The pattern is consistent: something fails in production, the founder has no way to diagnose it, it takes hours or days to find the cause by reading code instead of logs, and only then does the founder add monitoring. Don’t do this. The monitoring setup is a couple of hours of work. Diagnosing an incident without it can take days.
Structured error tracking - Sentry, Datadog, or any equivalent - gives you every unhandled exception with the full stack trace, the user context, and a count of how many times it’s happened. That turns “three users emailed saying payments aren’t working” into “there’s been a TypeError in the payment confirmation handler 47 times in the last two hours, here’s the exact line, here’s the user ID of the first person it hit.”
Structured logging - logging JSON objects with fields like userId, action, durationMs, statusCode - turns your logs from a wall of text into something you can query. “What did this user do in the 10 minutes before the error?” becomes a 30-second query instead of a manual log trawl.
The fix. Add error tracking before you need it. For Next.js, Sentry takes about two hours to instrument - its setup wizard (npx @sentry/wizard@latest -i nextjs) does most of it. Add structured logging to every route that touches payments, auth, or external services.
N+1 queries - the invisible database tax
N+1 queries don’t show up in code review, and they don’t show up in testing. They show up at peak load, as an emergency.
They happen when you fetch a list of records and then, inside a loop, run a separate database query for each record to load related data. A feed showing 20 posts with their authors fires 21 queries: one for the posts, then one per post to look up the author. At 10 users, this is fast enough that nobody notices. At 500 concurrent users, every page load multiplies that query count, and your database is doing many times more work than it needs to.
AI tools generate N+1 queries because they build ORM resolvers without full schema context. The code reads cleanly - there’s no obvious problem. The trouble is the query pattern it generates at runtime, which you can only see in your database query log.
This is one of the most common patterns we see in Code Rescue work on Cursor apps: the architecture is solid, but the query surface has N+1 holes throughout the data layer. The fix is mechanical once you know the pattern. Load parent records, then load all related records in a single query using the parent IDs, then join them in application code. In Prisma that’s the include option. In Django that’s select_related. In raw SQL it’s a JOIN.
The fix. Turn on query logging (log_min_duration_statement in Postgres, or your ORM’s debug mode). Look for clusters of near-identical queries with different ID values - that’s your N+1 surface. Fix with eager loading or explicit JOINs. An afternoon of work in most apps.
Schemas built for today’s features only
A vibe-coded app’s schema is built for the current feature set. The first time you need a breaking change - rename a column, change a type, add a NOT NULL constraint to a populated table - you find out what it costs.
Two patterns appear in most AI-generated schemas we audit:
Missing indexes on foreign key columns. AI tools reliably create the foreign key constraint but miss the index on the key column itself. A user_id column on invoices with a FK to users.id but no index means every query filtering by user runs a full table scan once the table is large enough. This is a silent performance drain that grows with data volume.
No soft-delete pattern. Apps that hard-delete records discover at the first compliance question - “we need all historical activity for this user” - that the history is gone. Adding deleted_at timestamps and filtering WHERE deleted_at IS NULL is table stakes for any app storing user-generated data. Bolting it on retroactively is a painful migration; building it in from the start is free.
Schema migrations on live tables cause downtime unless you use the expand-contract pattern: add the new column nullable first, backfill existing rows in chunks, then add the NOT NULL constraint once all rows are populated. The AI-generated “just ALTER TABLE” approach locks the table while it runs - fine on 1,000 rows, disaster on 1 million.
The fix. Before writing the next migration, index every foreign key column that doesn’t have one (Postgres doesn’t add these automatically). Add deleted_at to tables that store user-generated data. For any future migration that adds a constraint or changes a column type, use expand-contract instead of a single destructive ALTER.
How to prioritize the fixes
Not all five have the same urgency. Here’s the order we apply them:
- Before any more users: Auth hardening. RLS policies and server-side ownership checks. This is the fix that prevents a security incident from becoming the article about your startup.
- This week: Error handling. Wrap external calls, add structured error responses. This stops silent failures from piling up unnoticed.
- This week: Observability. Add error tracking. This is what tells you everything else that’s broken before users do.
- Next sprint: N+1 queries. Turn on query logging, find the hotspots, add eager loading. This is the fix that prevents database meltdown under growth.
- Before the next migration: Schema hygiene. Index your foreign keys, add soft-delete where it matters, and use expand-contract for any constraint or type change. This is what keeps your options open.
Auth is emergency work, and error handling and observability belong in the same week. N+1 queries and schema hygiene are growth maintenance. All five are faster to fix before the incident that makes them visible.
How a Zeroic Code Rescue works through the five
Our Code Rescue service is built around these five patterns. Every rescue starts with a 3-day review: a systematic pass through auth, error handling, query patterns, observability, and schema state. What we find sets the rescue scope.
For auth failures on a Lovable app, auth hardening usually takes 3-5 days: RLS policies on every table, ownership checks on every route that reads sensitive records, webhook signature verification. The TypeScript code in most Lovable apps is solid. The gaps are in what was left out, not what was written wrong.
For query and schema work at scale, our reference is FormulaBot / Better Analyst, which we helped take from thousands to 1.5M+ users. It wasn’t a vibe-coded app, but the lesson carries over: at a million users, N+1 queries and missing indexes don’t survive. The query and schema habits we bring to vibe-coded rescues are the ones that hold up at that scale.
Not sure how much of this your app needs? Start with a Security Audit: from $1k, 1-2 weeks, and you get a ranked findings report before you commit to anything more.
Frequently asked questions
- Which failure mode breaks most Lovable apps first?
- Auth. Specifically, missing or misconfigured Row Level Security on Supabase tables. The research behind CVE-2025-48757 found 303 endpoints across 170 Lovable projects (about 10.3% of 1,645 analyzed) with inadequate RLS. In an app like that, anyone holding the public key can potentially read other users' data until every table has RLS enabled with policies that match the app's logic.
- Are N+1 queries really a problem in vibe-coded apps?
- Yes, and they stay hidden until you're under load. AI tools generate ORM resolvers that load related records one at a time inside loops, so clean-looking code fires N+1 database queries. A feed of 20 items becomes 21 queries. A dashboard with 100 rows becomes 101 queries. Query logging makes N+1 queries visible in minutes; fixing them with eager loading usually takes an afternoon.
- How long does it take to add observability to a vibe-coded app?
- Adding Sentry for error tracking takes about two hours for a Next.js app - its setup wizard (`npx @sentry/wizard@latest -i nextjs`) does most of the work. The payoff is immediate: you'll know about every unhandled exception within minutes instead of finding out when users complain. Most founders put this off until after their first incident. Do it before.
- Can I fix these failure modes myself or do I need a developer?
- Some of them yourself, but not auth. Auth hardening - RLS policies and server-side ownership checks - needs SQL knowledge and backend experience, so get a developer involved. Observability setup with Sentry is self-serve. Error handling and N+1 fixes sit in the middle: you can add try/catch blocks yourself if you're technical, but getting the patterns right so errors aren't silently swallowed takes experience.
- What's the difference between Code Rescue and a full rewrite?
- A Code Rescue targets the specific seams that are breaking - auth hardening, query optimization, adding observability - without touching the product logic that works. A full rewrite makes sense when the architecture is so compromised that fixing one thing keeps breaking another. A Security Audit tells you which path fits before you commit to either.
References
- CVE-2025-48757 - Lovable RLS Vulnerability
- Matt Palmer: Statement on CVE-2025-48757
- Wiz: Exposed Moltbook database reveals millions of API keys
- Supabase - Row Level Security
- CodeRabbit: State of AI vs Human Code Generation Report
- PostgreSQL - Error reporting and logging (log_min_duration_statement)
- PostgreSQL - Constraints (foreign keys and indexes)
- Sentry - Next.js setup
- Prisma - Eager loading with include
Prashant Abbi