Skip to content
Back to blog
12 min read

What actually breaks in a vibe-coded app: the audit we run before we quote

The situation, stated plainly

You described an app to Lovable, Bolt, v0, Cursor or Replit, and it built one. It works. Customers can see it, maybe a few already pay for it. And somewhere between the demo and the first real users, you started to feel the floor flex: an odd bug nobody can reproduce, a page that is slow only on phones, a nagging awareness that you have never actually read the code that handles your customers' passwords.

That instinct is correct, and it is worth money to act on it early. When we take on software rescue work, we do not quote from a screenshot or a repo link. We run a fixed audit first, six gates, the same six every time, and the quote comes out of what the audit finds. This article is that audit, in the open, with the checks written so you can run most of them yourself before you talk to us or to anyone else.

One thing first, because it frames everything below. Several of the failure examples in this article are ours. They come from our own studio's engineering log, from the site you are reading, found by measurement and fixed with a regression guard. Careful teams ship these bugs too. The difference with AI-generated codebases is density and visibility: the same classes of defect appear more often, and there is no log telling you where they are. That reading matches the published research; MEV's roundup of the field cites work finding that AI assistance introduces a security flaw in roughly 45% of coding tasks. The figure is the industry's own; we cite it, we never measured it. The direction, though, matches what we find gate by gate.

Gate 1: count the authentication layers, then subtract the decorative ones

Generated apps almost always protect private pages with exactly one mechanism: a redirect. If you are not logged in, the router sends you to the login screen. It feels like security because the browser behaves. It is choreography, and choreography can be skipped: the redirect runs in client code or an edge layer, and both fold to disabled JavaScript, a direct API call, or one bad deploy.

Our own rule is three independent layers, each of which holds alone. A proxy redirects, which is convenience. The protected layout re-validates the session server-side on every render, which is the first real wall. And every single mutation re-checks the caller's identity before writing, which is the wall that still stands when someone bypasses the pages entirely and speaks to the API directly.

The audit questions are mechanical. Where is the session checked, and does any write path skip that check? What hashes the passwords, and can anyone on the team answer that in under thirty minutes? Do sessions expire? Is there a rate limit on the login endpoint, or can a script try passwords all night?

Check it yourself: log out, disable JavaScript, and open a protected URL. Then request the same URL with curl, no browser at all. If either response contains private data, your security is one layer deep, and that layer is decoration.

Gate 2: data rules the database enforces, versus rules the code promises

A rule that lives only in application code is a promise. A rule that lives in the database is a fact. Generated code is built almost entirely from promises, because promises are what a prompt produces: "check if the user already has a subscription before creating one" compiles into an if-statement, the if-statement works in the demo, and the demo never runs two requests at the same time. Production does. Two concurrent requests, two subscriptions, one very confused billing cycle.

A concrete version from our own uptime monitoring system. The requirement was "one alert email per incident". The first implementation kept that promise in the sending loop. The shipped implementation makes it a fact instead: a partial unique index in Postgres declares that at most one open incident can exist per site per failure type, and a stamp column records that its alert went out. The database now refuses the state the loop used to merely avoid. Cron fires twice, network hiccups mid-run, none of it matters; a duplicate is impossible rather than unlikely.

In a vibe-coded schema you will find very few constraints, because nobody prompts for them and their absence costs nothing until load arrives. You will also, frequently, find no migration files at all: the schema was assembled by clicking, which means its current shape cannot be reproduced, reviewed or rolled back.

Check it yourself: open your schema and count the UNIQUE, CHECK and FOREIGN KEY declarations outside primary keys. A near-zero count means every business rule you have is a race condition with good manners. Then look for a migrations directory. No directory, no history.

Gate 3: the three security findings that take minutes

Three checks open every audit because each takes minutes and each is routinely open.

Where the keys live. Generated code gravitates to the most powerful credential available, because the powerful one makes everything work on the first try. If an admin-level database key (Supabase's service_role is the common case) has reached client-side code, your entire database is publicly writable to anyone who opens dev tools. Search your built JavaScript bundles for your secret values. A hit is a same-day rotation, and then a redesign of who talks to the database with what.

Row-level access. Postgres-backed platforms give you row-level security; the question is whether it is enabled on every table and whether the policies say anything. A table with RLS off means any authenticated user can read any row, including other customers'. We have watched this survive months in apps whose login flow was otherwise fine, because the login worked and nobody asked what the token was allowed to do afterwards.

Inputs and webhooks. Every field a user types into and the app later renders is a stored-XSS candidate; our own blog pipeline runs every stored body through a sanitizer before it touches the page for exactly this reason. Every webhook endpoint a third party calls needs its signature verified over the raw request body, before parsing, in constant time. Generated handlers usually parse first and check never, which is an internet-facing parser with no bouncer.

Check it yourself: grep the repo and the built output for your keys, list which tables have row security disabled, and read your webhook handlers top to bottom: does anything execute before the signature check? If any of this is uncomfortable to do alone, our free AI audit exists for precisely this stage, and a human reads the result before anything is sent.

Gate 4: backups, where the only question is restore

Backup conversations stall on the wrong question. "Do we have backups" is answerable with a dashboard screenshot. The three questions that matter are: what is actually inside them, how long are they kept, and have you ever restored one.

We ask them of ourselves, and our own operations docs record two uncomfortable answers. First: our daily database backups do not include uploaded files, because storage objects are not part of a database backup; a file-upload feature without its own backup path is data with no copy, and ours was, until we wrote that sentence down. Second: our restore drill document exists, step by step, and its status line still reads NOT PERFORMED. Until it runs once, our backups' existence is verified and their restorability is a belief. We publish that line in our own checklist because an unverifiable claim marked honest is worth more than a ticked box.

A vibe-coded app typically stalls on question one: whatever the hosting platform does by default, and nobody knows what that is.

Check it yourself: write down, in one sentence, what would happen if your database were deleted right now. If you need to open your provider's documentation to finish the sentence, the real answer is "I don't know", and that is the finding.

Gate 5: observability, because silent failure is the default

The most common production state we find is the silent error: the app is up, something is broken for users, and no system tells anyone.

Here is how quiet this class can be, from our own site. For a stretch of this year, every non-existent URL on our marketing pages answered HTTP 200 instead of 404. The cause was one innocent-looking loading-screen file: it wrapped every page below it in a boundary that committed the response status before the not-found branch could run. A visitor saw a perfectly normal error page. A crawler saw success. Nothing looked wrong anywhere, and one curl -I exposed it instantly. We deleted the file, measured both ways against a production build, and added a test that fails if anyone reintroduces one on that route tree.

The same gate covers error tracking and uptime. No error tracker means your users are your monitoring, and most users skip the bug report and simply leave. No uptime check means your customer tells you the site is down, which is the most expensive alerting system on the market.

Check it yourself: curl -I a URL on your app that certainly does not exist and read the status code. Then answer in one sentence: if your payment flow starts throwing at 2 a.m., what wakes you up?

Gate 6: a green pipeline proves what it measures

Two halves. The obvious half: generated MVPs ship with no tests, and before real users arrive you want coverage on the paths that touch money, sign-up, payment, permissions. Not everything. Those.

The less obvious half is the one that bites teams who think they are covered: a green pipeline guarantees only what it actually measures. Our own case: our translation layer renders a key's path instead of throwing when a string is missing, so three admin screens went to production with literal adminFiles.sectionTitle text where their headings should have been, while typecheck, lint, tests and build were all green. Every gate passed because no gate measured that. The fix was a purpose-built check that resolves every translation key in both languages, wired into CI as a blocking step. The lesson generalises to any generated codebase: "it compiles and deploys" is not a quality gate, it is a syntax gate.

Check it yourself: does your repo have a CI configuration at all? If yes, list what it runs. If the list is "build", your gate certifies that the code compiles, and nothing else about it.

What only measurement shows

Two closing examples from our own log, because they share a property the previous six gates only hint at: some defects are invisible in code review and in screenshots, and exist only in measurement.

The first: our home page's 3D visual was desktop-only, sitting in a container hidden on phones. The chunk behind it downloaded to every phone anyway, 877,982 bytes raw, 231,452 gzipped, because dynamic imports fetch when a component mounts and CSS visibility has nothing to do with mounting. Every mobile visitor paid a quarter-megabyte toll for an element they could never see. The page looked perfect. The network tab did not.

The second: an animated element was driven with a GSAP helper (quickTo) that animates one numeric property and silently ignores the visibility property we handed it. The element tracked the pointer flawlessly, with correct coordinates on every frame, and remained invisible forever. No error, no warning, motion logic demonstrably running. Only a pixel-level check caught it.

Vibe-coded apps are dense with this category, because generation optimises for "runs and looks right", and neither of those is "measured". Part of our audit is exactly this: bundle weights per route, what actually loads on a phone, real status codes, real Core Web Vitals. Numbers, then opinions.

What a rescue costs, honestly

Market context first, so you can calibrate any quote you receive, including ours. Published market surveys of this exact niche (AppStuck's playbook and MEV's agency roundup) put fixed-price rescue work typically at $25K–$50K over 4–8 weeks, boutique hardening at 2–6 weeks, and standalone audits offered from around $1,500. Those figures describe the market, and they are cited here for calibration; our own number comes out of the audit.

Our own process is the one this article just walked through: audit first, then a fixed price and a fixed deadline in writing, because a rescue quote made before the audit is a guess with a signature on it. Sometimes the finding is that a rescue is the wrong buy and a rebuild on a proper foundation is cheaper than surgery; our web application development work exists for that outcome, and we will say so when it is true, because a rescued codebase we cannot stand behind costs us more than a lost invoice.

Run the six checks first

The whole audit, compressed to a list you can run this afternoon:

  1. Logged out, JS disabled, curl: does any protected URL leak data?
  2. Schema: how many real constraints? Is there a migrations directory?
  3. Grep for keys in the client bundle; list tables with row security off; read the webhook handlers.
  4. One sentence: what happens if the database is deleted now?
  5. curl -I a non-existent URL; one sentence on what wakes you at 2 a.m.
  6. What does CI run, and is any of it more than "build"?

Whatever passes, you have earned some sleep. Whatever fails, write to us at the software rescue page with the failing gate numbers, and you will get a reply within 24 hours that tells you what fixing them involves, before anyone talks about a contract.

Need a development partner?

Brecon builds custom software and AI processes for growing companies at a fixed price. Write to us and we reply within 24 hours on a working day.

What actually breaks in a vibe-coded app: the audit we run before we quote | Brecon