I let an AI agent build a SaaS blind. Here's the cross-tenant leak it shipped.
I audit AI-built apps for the people shipping them, so I wanted to know something concretely, not in theory: if you let an agent drive development blind — idea, spec, code, all of it — and you only ever check "does the feature work?", what does it actually ship? Not a toy. A real, multi-tenant SaaS with accounts and billing.
So I ran the experiment. This is what it shipped, and the one bug that matters.
The experiment
I gave an AI agent the wheel and kept my hands off the code. The agent came up with the idea, wrote the specification, and wrote every line. My only job was the job a real "vibecoder" actually does: run the thing, see if a feature works, and tell the agent what's broken in the product. I never cleaned up the code. I never reviewed a diff for correctness. If it ran and did the thing, it passed.
This is roughly how a lot of software gets built in 2026, and I wanted to audit that reality, not a tidy codebase someone had already combed.
What it built
It built something that worked, which is what makes it dangerous.
The agent produced an open-source core first (a single-tenant tool that stores everything in local SQLite — one user, nothing to keep separate). Then it built a hosted SaaS layer on top: it swapped SQLite for Postgres on Supabase, added multi-tenancy so each account is its own world, user settings, and billing.
Everything worked. You could sign up, log in, add your data, see your dashboard, pay. The demo looked finished. If "it works" were the bar, it cleared it.
Where the layers diverged
This next part is worth slowing down for, because it's the most common and most damaging failure in fast-built multi-tenant apps, and it's invisible from the outside.
The app had a login. It had authentication. It had the database's row-level isolation switched on in the configuration. Three correct-looking pieces. And it still leaked one account's data into another's, because those three pieces never added up to one real boundary.
Two things had lined up wrong:
1. The database isolation was on, but pointed at nothing. The generated policy scoped each row to "the current user" — something like this (synthetic, simplified):
-- looks correct in isolation…
create policy tenant_isolation on records
using (tenant_id = auth.uid());
The problem: auth.uid() is a value that exists in the app's login layer, not
on the raw database connection the server actually uses. Over that connection
it's empty. So the policy that was supposed to be the wall had no one to check
against. It was switched on and guarding nothing.
2. The application had stopped guarding itself. Earlier iterations of the code filtered every query by the owning account explicitly. Somewhere along the regeneration path, the agent dropped that filter. Reasonably, from its point of view, because "the database handles isolation now." Except it wasn't.
Neither layer actually enforced isolation, and each looked fine on its own. The login worked, the policy was enabled, the queries returned data. Every check you'd glance at looked green. The failure was in the seam between them: a signed-in account could list, read, and delete rows that belonged to other accounts, and reach data that was never theirs.
The agent was never given a rule that said "prove account A can never see account B's rows," so nothing enforced it. It built every piece it was asked for and shipped the gap between them.
Why a scanner doesn't catch this
An automated scan reads "row-level security: enabled" and prints a green check. It's not wrong about the setting. It's blind to the intent. It doesn't know what your app is supposed to keep separate, so it can't tell that the wall is facing the wrong way.
The point worth remembering: automation pattern-matches configuration; it doesn't know what "tenant isolation" is supposed to mean for your app. That gap is where this class of bug lives. The only thing that finds it is a human reading the code with one question in mind, "can account A reach account B's data?", and refusing to move on until the answer is provably no.
How I proved it and closed it
This is where the experiment ends and my real work begins. It's the part a vibecoder is paying for. Everything above, an agent can do: conceive, spec, build, ship. What comes next is the part it can't do: prove the leak is real, close it so a later regeneration can't reopen it, and make that permanent.
I didn't want to "fix it and trust myself." I made the gap provable, then permanent.
First, I proved it. I wrote a test that creates two separate accounts and asserts each can only ever see its own data, including the delete path. Against the shipped code, it failed, the way a real leak fails:
before → Tests 4 failed | 2 passed (isolation broken)
Then I closed it with two independent layers, so a single future mistake can't reopen it:
- Every account-scoped query filters by the owning account again, explicitly — independent of any database setting.
- The app now runs under a non-privileged database role whose isolation policies key on a value that is set on the real connection, so even a query that forgets to filter stays confined to the right account by the database itself.
Then I locked it in. The isolation tests now run on every change, against a real database, in CI. This specific hole cannot silently come back on the next regeneration. Closing the second layer meant adding four more tests just for it, so the count goes up: the original six now green, plus four new ones proving the database confines an unfiltered query on its own:
after → Tests 10 passed (10) (the original 6, now green, + 4 new for layer 2)
Why this matters even if you never touch the code
Most founders miss this. In vibecoding, a regeneration isn't an edit. It's a lottery. The next prompt can silently remove a safeguard the last one added, and nothing will look different in the demo. That's not hypothetical: Lovable-built apps have been breached in the wild, and in plenty of cases the app had been "checked" at some earlier point, and the hole arrived in a later iteration, after the check. A snapshot is not protection.
If your app has accounts, and an agent built it fast, the question isn't whether it runs. It's whether one customer can reach another's data, and whether anyone has proven, with a test that stays in place, that they can't.
That's the class of problem I look for, and it lives where the automatic tools can't reach. If that sounds like your app, here's how I'd check it.