Sample Rescue Audit: A Vibe-Coded ERP

People ask what a Rescue Audit actually gives them. Easier to show than to describe, so here is a real one, redacted.

The subject is a construction ERP built with an AI app builder on top of Supabase: roughly 15,000 lines of TypeScript, 61 source modules, 34 database tables, 23 migrations, four user roles (admin, site manager, procurement officer, accountant). It tracks projects, suppliers, materials, job costs and variation orders: the documents that change what a contract is worth. Real money runs through it.

We audit against the failure sequence we published in what breaks first when a vibe-coded app meets real users, plus maintainability and operations. Same seven questions, every time. Here they are answered.

On redaction: the app belongs to a real business, so it isn't named and no exploit steps appear. The owner was told privately first, with the findings and a remediation plan, at no charge, before a word of this was published. That order is not negotiable, for them or for any client of ours.

The root cause, and it isn't in the code

We check how code reaches production before we judge the code, because the workflow explains the codebase. Here is what this project's version history says.

Read those together and the picture is unambiguous: nothing has ever been inspected between the model's output and production. No review, because there are no pull requests. No automated check, because there is no CI. No record of intent, because the messages say nothing. The code did not go wrong because the model is bad at writing code. It went wrong because nobody, human or machine, was ever positioned to say no.

This has a second cost that surfaces later, during the first real incident. A history in which 171 of 231 commits are called "Changes" cannot be bisected, questioned, or explained. When something breaks in October, the question "what changed, and why?" will have no answer. The audit trail every team leans on in a crisis was never written.

Every finding below is a symptom of this one. The permissive database policies, the disabled compiler checks, the tests nobody runs: none of them are mistakes somebody made. They are what accumulates when no gate exists. Fix the gates and the codebase stops getting worse; fix only the findings and you will be re-writing this report next quarter.

The scorecard

1. Authorization: the UI has roles, the database doesn't

This app does what most vibe-coded Supabase apps forget: it turns Row-Level Security on, across all 34 tables. Scanners show green. The dashboard shows green.

But of its 97 policies, 44 are permissive: 39 on tables and 5 on document storage. They grant access to any authenticated user, a blanket true with no identity check at all, and they cover the tables that matter: projects, suppliers, materials, stock movements, job-cost lines, and variation orders. Meanwhile the React app checks the user's role in 42 different places before showing a button.

Those two facts collide because of something every Supabase app has: the public API key ships inside the browser bundle, by design. That's fine and expected. It does mean the React UI is not a gate, it's a suggestion; the database is the only real gate, and on those tables it is currently open to anyone with a login.

The same pattern extends to files, which is where we nearly missed it. The document storage is correctly private to the outside world, but its five policies check only which bucket you are reaching into, never who you are, so every employee can read and upload every document in the system: contracts, invoices, receipts. The roles were designed once, in the interface, and then not enforced anywhere the data actually lives.

Honest severity: high, but bounded by who can log in. Today the app has employee logins only, so this is an insider-risk finding, not a public breach. A site manager can rewrite the accountant's job costs, or alter a variation order and change what a contract is worth. That may be tolerable in a small, trusted office. What it means concretely is that the role system is decorative: four roles were designed, and the database enforces none of them. And the day a subcontractor or client gets a login, the same gap stops being an insider problem and becomes a breach. Fix it while it is cheap.

2. Payments and webhooks: not applicable

No payment processor, no webhooks. We report this rather than skip it, because "we looked and there's nothing here" is a finding too, and because the money in this app moves as documents (variation orders, job costs) rather than as card charges, which is exactly why item 1 matters more than it would elsewhere.

3. Data integrity: the strongest part of the codebase

45 foreign keys, 60 check constraints, 160 not-null columns. The schema genuinely defends itself, which is rare in an AI-generated app and deserves saying plainly: whoever drove this cared about the data model.

One real gap. Job-cost lines, variation-order lines and stock movements are written from the browser as multi-step sequences. If the network drops between steps, the database keeps whatever arrived. Financial records should move as one unit or not at all. Severity: medium, and it is a contained fix.

4. Performance: fine today, queued for later

31 queries fetch every column of a table, and only two places paginate. At an early project's data volumes this is invisible. At ten times the job-cost lines, the materials list and the cost sheets are the first screens to crawl. Severity: low now, medium later, which is precisely the sort of thing an audit exists to schedule rather than panic about.

5. Secrets: the scary thing that isn't

An environment file is committed to the repository. Every scanner flags this as critical; a human downgrades it in ten seconds, because the contents are the public API key and project URL, which are public by design.

It still needs fixing, for a subtler reason: the repo has no example environment file, so committing the real one is now the project's onboarding convention. One day that file will contain a key that is secret, and the habit will already be in place. Severity: low, fix it anyway, because you are removing a trap door, not a bug.

6. Observability: users see the failures, the owner never does

Credit where due: the app surfaces errors to users, with toast notifications throughout. People are told when something goes wrong.

But there is no error tracking of any kind. Nothing collects those failures, counts them, or tells the owner they happened. The app makes 173 database calls; about 51 check whether the call actually succeeded. The rest can fail quietly. So the failure modes that matter most, the silent and the intermittent, are invisible to the only person who could prioritize fixing them.

Severity: high, and it is the first thing we would fix, not because it is the worst problem, but because until it exists every other priority in this report is an educated guess, including ours.

7. Tests and CI: good seeds, nothing watering them

There is a test suite, and the tests are on the right things: budget summaries and job-cost payload construction. Someone chose to test the money. That instinct is correct and worth keeping.

The problem is scale and enforcement. Five real tests cover pure functions in a codebase of 61 modules; ten feature areas (projects, materials, suppliers, budgets, documents, and more) have none at all, and nothing tests the database interaction, the permissions, or a single component. Worse, no continuous-integration workflow exists, so nothing runs even the tests they have. They can rot silently, and no regression is ever blocked before it merges.

Severity: high, cheapest fix in the report. The hard part, deciding what to test, is already done. What is missing is a configuration file that runs them on every change.

One caveat on sequencing, because it is a mistake we see teams make: continuous integration is not automatically a gate. Wire it up on a project that pushes straight to the main branch and it will faithfully run the tests after the code has already shipped, which is a smoke alarm that rings once the house has burned. Tests only become a ratchet when something can block a merge, and blocking a merge requires there to be a merge. That is why the roadmap below starts with a gate, and CI second.

8. Maintainability: the safety net is switched off

The quiet finding, and the one most likely to be dismissed. The project's TypeScript configuration disables strict mode and implicit-any checking, and there are 125 uses of the escape-hatch any type across the source.

This is the default that AI app builders ship, and almost nobody changes it. It costs the project two distinct things, and they are worth separating.

Because implicit-any checking is off, any value the code does not explicitly annotate quietly becomes any, and any is where the compiler stops looking. In those places, and in the 125 written by hand, TypeScript will not tell you that the field you renamed is still referenced in four other files, because it no longer knows what the thing is. Elsewhere, where types are honest, it still would. So the safety net is not gone; it is full of holes, and the holes spread, because every untyped value infects what it touches.

The larger cost is quieter. With strict mode off, null-checking goes with it, so the compiler will never mention that a value might be missing. That is the origin of the most common crash in all of JavaScript, the one that reads "cannot read properties of undefined," and in this app it means a blank screen on a cost sheet when a record comes back empty. The compiler could have caught every one of those before they shipped. It was told not to.

Severity: medium, but compounding. This is a root cause, not a symptom. Turning strict mode on will surface a pile of latent problems at once, which is exactly why it wants to happen after error tracking and CI exist to catch what it shakes loose. There are also a handful of oversized components (one form file runs to 695 lines) that will resist change until they are split, though none are emergencies.

9. The findings a scanner would scream about, in context

Two more, both of which an automated tool would rank critical and a human ranks low. We include them because knowing what not to panic about is half of what you are buying.

The privileged server function accepts requests from any origin. That looks alarming, and in a cookie-authenticated app it would be. This one authenticates with bearer tokens instead, so the permissive setting cannot be abused as written. It should still be tightened, because it stops being harmless the moment anyone changes how the app logs in. Low.

Three build-tool dependencies carry published advisories. None of them run in front of your users; they run on the developer's machine and in the build. Update them on the next quiet afternoon. Low.

10. The duplicate-supplier problem waiting to happen

The schema has only three uniqueness rules in the entire system. Nothing prevents the same supplier, material or cost code from being created twice under slightly different spellings. In an ERP this is a slow poison rather than an outage: six months from now the cost reports quietly split across "ACME Ltd" and "Acme Limited," and nobody can say which number is true. Medium, and cheap to prevent, expensive to unpick later.

What was already right

An audit that only finds fault is a sales pitch. It is also less useful: knowing which parts you can stop worrying about is half of what a report is for. This codebase gets real things right.

The verdict: harden, do not rewrite

It is the question every founder asks at this point, so an audit should answer it plainly. An audit has three honest answers to it: harden in place, replace one subsystem behind its interface, or, rarely, rebuild from scratch. For this app the answer is harden, and here is the evidence.

The data model is good: 45 foreign keys, 60 check constraints, a schema that defends itself. Somebody made real decisions here, and a rewrite would begin by throwing them away. The business logic works; it is in production, being used, with the accumulated corrections of every bug the team has already hit. And nothing in the findings above is structural. Not one of them requires a different architecture. They are a missing review gate, absent observability, permissions written too loosely, and a compiler that was switched off. Every one is fixable in place, most of them in days.

A rewrite would also inherit the thing that actually caused this: the workflow with no gates in it, the same bot pushing straight to main with no one reading the diff. That is the general case we make in should you rewrite your vibe-coded app, which is why a fresh start here would arrive, months later, at a fresher-smelling version of exactly this report.

The roadmap we would hand them

Sequenced, cheapest and most enabling first, each step making the next one safer. The order is the same one we published in how to harden an AI-generated codebase, which is the point: the method is not invented per client.

  1. A gate, any gate

    Branch, pull request, one human read before merge. Even a solo founder reviewing the model's diff catches the worst of it, and the history starts being able to answer questions.

  2. Error tracking

    Half a day. Turns every priority below this line from a guess into evidence, including this roadmap.

  3. CI on the tests that already exist

    An afternoon. Installs the ratchet: from here on, protected behavior can only change deliberately.

  4. Permissions, money tables first

    Variation orders and job costs before anything else. Reuse the role helpers already written; decide per table who may read and who may write. Encode the rules as tests so the ratchet holds them.

  5. Transactions around financial writes

    Money moves as one unit or not at all. Contained, and it closes the partial-write class of bug permanently.

  6. Strict mode, incrementally

    Now that failures are visible and tests are enforced, turn the compiler back on and work through what it finds, file by file. This is the change that makes every later change cheaper.

  7. Then the deferred debt

    Pagination on the growing tables, environment hygiene, splitting the oversized forms. Scheduled, not urgent.

A note on method: scanners lie in both directions

Worth telling on ourselves. The first automated check we ran reported that this app had no row-level security at all. It was wrong: the tool had not indexed the files it claimed to have read. Had we trusted it, this report would have opened with a false alarm.

The reverse trap sits right beside it. The platform's own dashboard correctly reports that row-level security is enabled on every table, and had we trusted that, we would have called the app secure and missed the actual finding.

One tool said the sky was falling, another said everything was fine, both were reading the same code, and neither was right. That gap is the whole reason a human does this work. An audit's value is not the tool output; it is the judgment about which findings are real, which are noise, how bad each one honestly is, and what order to fix them in.

What this would look like for your app

The same thing, on your codebase: one week from an agreed start, fixed price, ranked findings across security, correctness and maintainability, plus a sequenced roadmap you could hand to any team, including not ours. If your AI-built app has users, it has a version of this report waiting to be written.

Book an intro call Read: what breaks first