A friend showed me an app he’d built with one of the AI builder tools — the kind where you describe the thing in plain English, agree with it twice, and twenty minutes later you have a working signup flow, a dashboard, and a database you didn’t personally touch.
It worked. People signed up. Forms submitted. The dashboard loaded. He was proud of it, and he should have been — building this thing by hand would have taken him weeks he does not have.
Then it started losing records.
Not all of them. Not every time. Not in a way that logged an error anywhere. Just enough that a user eventually noticed that something they’d done last Tuesday was no longer there, and asked about it. A loan with no payment schedule. A payment schedule pointing at a loan that didn’t exist. The kind of ghosts that only show up in a database that was built the way you’d build a spreadsheet.
He asked me to look at it. I spent an afternoon reading what the AI had assembled, and then I did something that felt rude but turned out to be the only move: I did not try to patch it. I rebuilt it from scratch, in a different repo, without looking at the original schema or the original code again.
What the diagnosis actually found
The problem was not any one bug. It was the shape of the whole thing.
The AI builder had done an impressive job of the parts you can see. The forms validated. The pages loaded. The dashboard pulled numbers that mostly added up. All of the demo-grade work was there, and all of it was fine.
What it had not done was build anything that could hold the state together when the UI wasn’t watching.
Writes raced each other. A loan was two separate INSERT statements against two tables, and nothing bound them into one unit of work. If the request died between them, you got half a loan. The foreign keys that would have refused to let the second half live without the first were not enforced. Nullable columns were being used to mean three different things depending on the feature. The schema had drifted — every new feature the AI added was a new table and a new join, loosely wired to the old ones, never rationalized. There was no concept of a transaction anywhere in the data layer.

I kept thinking about the pattern this represents. The tool was not sloppy on purpose. It had done, every time, the smallest thing that made the demo pass. Add a column. Spin up a table. Return the row. Move on. There was no moment in the whole build where anything stepped back and asked “what are the invariants here, and what refuses to break them.”
That is the gap. Not that the AI code was bad. That nothing in the loop had the job of being the floor.
Vibe-coded apps are built with the vibes of someone who has never had to clean up a database.
— Me, writing this
Why the rebuild had to be clean-room
The temptation when you look at an app like this is to patch it. Add a transaction here. Add a foreign key there. Backfill the orphans. Ship a fix.
That doesn’t work, and the reason it doesn’t work is subtle. The schema is not the problem on its own. The schema is a record of every tactical decision the model made across a dozen prompts, each of which was trying to get past a specific error in a specific moment. A column got added at 2am because the model couldn’t figure out how to express an optional relationship. A junction table showed up because something somewhere was failing a lint. Half the fields exist to work around the other half.
Patching this is like trying to fix a bridge by replacing one bolt at a time while traffic is on it. The structural problem is not any single bolt. It is that nobody ever drew the bridge.
So I threw it away. Not angrily — gratefully, actually, because the AI build was a very good product spec written in the form of a working artifact. I read it to understand what my friend wanted the app to do, and then I started a new repo and built it against that goal.
- Afternoon 1DiagnosisRead the running app the way a user would, then read the schema the way a DBA would. Found the mismatch between the two.
- Afternoon 1DecisionClean-room rebuild. Keep the product spec that the AI build was really a stand-in for. Throw the implementation away.
- Day 2Floor firstMigrations, constraints, transactions. The data layer refuses bad data before the UI gets a vote.
- Day 2Domain layerA thin layer between the UI and the database. The UI can read what it wants; it can only write through the domain.
- Day 3IdempotencyKeys on every operation that touches money. Retrying a request can never double-charge or double-write.
What the new app actually has
Four things, none of them clever:
- An actual schema, defined in migrations, that lives in version control. Not “whatever the AI added last.” Every column has a reason and a constraint. Foreign keys are enforced. Nullables mean one thing.
- Transactions wrapping anything multi-step. The loan and the schedule write in the same block or neither of them writes. If the second insert fails, the first rolls back. The database refuses to carry half a loan.
- A domain layer between the UI and the data. The UI can read whatever it wants off the database. It cannot write directly. Every write goes through a function that knows the business rules and refuses to run operations that would break them.
- Idempotency keys on anything that touches money or sends a notification. Hitting the same button twice does not charge twice. A retried webhook does not double-apply a payment. The system assumes the network will lie to it and plans accordingly.

None of this is impressive engineering. All of it is boring engineering. The impressive part — the UI, the forms, the dashboard — the AI had already given us for free. The boring part was the whole job.
What the pattern is really telling us
I don’t think this is a knock on AI builders. They are doing something real. If my friend had tried to describe this app to a dev shop, he’d have gotten a quote that would’ve killed the idea, and then he never would have found out whether anyone wanted the app at all. The AI builder let him find out. That is a legitimate function.
What it isn’t, yet, is the floor. The floor is the part of the system that refuses to let the demo lie to you. The floor says a loan without a schedule is not a loan. The floor says a payment cannot be recorded twice because somebody double-clicked. The floor is a thing you still have to build yourself, and the gap between a working demo and a floor is where most of these apps are quietly losing data right now.
So the question I’d leave with anyone who just shipped something an AI builder gave them: do you know, right now, how many of your users’ records are shaped wrong? Because the UI isn’t going to tell you. The UI’s job is to look fine. The database’s job is to tell the truth, and if nobody wrote the rules it uses to tell the truth, it won’t.
That’s the part the AI didn’t build. Not because it couldn’t. Because nothing in the loop asked.