Cross-tenant data leak
Your customers can see each other's data. It is never just the one screen you found.
If one customer has seen another's records, treat it as a category, not a single bug. AI builders assemble the interface first, which is the right way to get a product into your hands quickly, but it means isolation tends to live in the interface: lists look filtered, admin pages look hidden, and the database underneath answers anyone who asks it directly. The route you found is rarely the only one. Here are the six places I check first, and the test that proves they are closed rather than merely quiet.
Fixed £4,500 · 3 days · pass-or-refund · next slot w/c 3 August
What actually breaks
- One permissive policy overrides every strict one you wrotePostgres row level security policies are permissive by default and combine with OR: a row comes back if any policy allows it. So the careful organisation check you added last month is silently undone by an early policy that only asks whether the caller is signed in. I read every policy on every table, not just the newest. And if you are on plain Postgres with no RLS, or a platform with its own entity rules, the same question applies in that layer: what exactly does a signed-in stranger get?
- Queries scoped to the record, never to the organisationThe generated query fetches by record id and nothing else: select from invoices where id equals whatever the browser sent. It works perfectly in a demo with one account, because there is nobody else's data to leak. The fix is structural, not per-endpoint: the organisation filter belongs in the data access layer, so a query without a tenant id fails loudly, instead of relying on every generated endpoint remembering to add the same where clause.
- The record id is the only credentialInsecure direct object reference is a grand name for a simple failure: the server checks that you are logged in, then fetches whatever id you supplied. Your customers never see the wrong id because your interface only links to their own records. But the API is not your interface. Anyone can call it directly with someone else's id, taken from a shared link, a forwarded email, or a browser history, and the server obliges.
- IDs you can count throughSequential integer ids turn the previous problem into a bulk export. If your records are numbered 1041, 1042, 1043, a short loop enumerates your entire customer base in minutes, no guessing required. Switching to UUIDs makes enumeration impractical and is worth doing, but it is camouflage, not authorisation: any id that leaks still unlocks the record. The ownership check on the server is the fix; the id format only limits how fast the damage spreads.
- Admin routes that check the interface, not the callerAI builders often gate the admin area by hiding the link, or behind a flag the browser holds. The endpoints underneath, user lists, exports, role changes, accept requests from any session, or none. Unlisted is not protected: the 2025 Base44 incident, where Wiz reached private apps through the platform's own undocumented auth endpoints, proved that point at platform level, and your app's hidden routes fail it at app level. Every admin endpoint needs a server-side role check on every call.
- Filtering done in the browser after the server sent everythingOpen dev tools, load your dashboard, and read the API response. If it contains every organisation's rows and your code filters them before rendering, isolation is a courtesy of your own frontend. Anyone calling the endpoint with curl gets the lot in one response, and nothing in your logs distinguishes that from normal traffic, because it is normal traffic. Frontend filtering is a rendering decision. Authorisation happens on the server or it does not happen.
How I prove your tenants are actually isolated
The fix is not credible without an adversarial test, so I write one first. Two real accounts in two organisations. I drive the app as customer A with a proxy recording every request the browser makes: pages, API calls, database queries, exports. Then I replay each request as customer B, substituting A's record ids, and again with no session at all. On Supabase I also query the data API directly with the public key, because that route skips your application code entirely. The test asserts one thing: zero rows cross the boundary by any route. It stays in your repository and runs on every deploy, so the next feature you prompt into existence gets held to the same standard.
What “done” looks like
We agree one of these in writing before you pay anything. If it passes, the sprint passed. If it doesn't, you get a full refund and keep the work.
- Replaying customer A's recorded requests under customer B's session, or under no session, returns errors or empty results, and that replay lives in your repository as a test, not a one-off exercise.
- Swapping in a neighbouring or guessed record id, with a valid login or none, produces an authorisation error, never the record.
- Every admin action is refused server-side for a non-admin account, including calls made straight to the endpoint with no interface in between.
- No response your API returns mixes rows from two organisations, checked by reading the raw responses rather than the rendered screens.
We agree the acceptance test before any work starts. If it doesn't pass within three working days, you get a full refund and keep every change I've committed. The risk is mine, not yours.
Other things that block launches
- Your Lovable app works in the preview. It breaks in production.
- Stripe took the money. Your customer got nothing.
- Your Bolt.new app runs fine in the preview. The published version doesn't.
- Your Replit app works in the workspace. The published version doesn't.
- Your v0 app looks ready to ship. The part that breaks is the part you can't see.
- Your Base44 app works when you use it. It breaks when your customers do.
- Your Cursor app runs on your machine. That is the only place it has ever been tested.
- Users can't sign in. It worked every time you tested it.
- It runs on your laptop. It will not deploy anywhere else.
- Your AI feature is impressive when it works. You can't put it in front of a customer.
- The full The Launch-Blocker Sprint