Guide

Can one of your customers see another customer's data?

VibeSafe · October 3, 2026 · 6 min read

Most security advice for AI-built apps is about secrets — keys in the source, a committed .env. Those are real, and they are also the easy ones. The failure that actually ends a small SaaS is quieter: customer A logs in and sees customer B's rows. Your app works perfectly. Every page loads. Nothing in your dashboard looks wrong.

Why AI builders produce this specific bug

Ask any AI coding tool for "a table of orders with a page to view them" and you will get working code in seconds. The generated query says: fetch the orders. It does not say: fetch the orders belonging to the person asking, because nobody mentioned that, and the demo works either way — when you are the only user, every row is yours.

The bug only becomes visible when you have two customers. By then you have shipped.

Two things usually have to go wrong together, and both are easy to get wrong in a Supabase-backed app:

Either alone is survivable. Together they mean the public key in your frontend bundle — which is public by design, visible in any browser's network tab — is enough to read the whole table.

The trap: RLS that is enabled and still does nothing

When we scanned 1,163 files from 393 AI-built repositories, 39 contained a policy written as using (true).

alter table orders enable row level security;
create policy "read orders" on orders for select using (true);

Read that policy as the database reads it: for every row, is true true? Yes. Return it. The dashboard reports row-level security as enabled, the checkbox is ticked, and the table is as open as it was before. This is what "add RLS to my tables" often produces, because it is a literal answer to the question asked.

What it needs to say is who the row belongs to:

create policy "read own orders" on orders
  for select using (auth.uid() = user_id);

And the same consideration applies to writing, which is the half people forget. A read policy stops someone reading another customer's order. It does nothing to stop them updating one.

How to find out, in three escalating steps

1. Look from outside, with no login

The fastest check needs no credentials and no setup: visit your own app as a stranger would and see what comes back. Paste your app's address into the Data Access check and it will report which tables answer a signed-out request, which API routes return personal data to nobody in particular, and which endpoints are reachable without a session. It reads only — nothing is written, nothing is changed. Free on every plan.

One honest limitation: from outside, an empty table and a properly protected table look identical. Both return nothing. That is why the next step exists.

2. Read your own policies

Open the SQL editor on a copy of your project and look at what the policies actually say, not whether RLS is ticked. You are looking for three shapes: tables with RLS off, tables with RLS on and no policy at all, and policies whose condition does not mention the user — using (true) being the common one.

3. Prove it with two users

A policy that looks correct and a policy that is correct are different things, and reading SQL is not proof. The only real answer comes from trying it: create two accounts on a staging copy, sign in as the second, and attempt to read, change and delete the first one's row.

That is what the two-user test does. It signs in as both accounts and works through the matrix one cell at a time — can B read A's row, change it, delete it, write a row as A; can a signed-out visitor read it — and gives you a grid of what was allowed and what was blocked, plus the SQL for anything that should not have been allowed. It runs only against a staging project you confirm, refuses service-role keys outright, and deletes its own test rows afterwards.

If you find something

Check what a stranger can read →

Free on every plan · No card needed · Read-only

Questions

Is my Supabase anon key supposed to be public?

Yes. The anon key is designed to ship in your frontend and is visible to anyone. It is not a secret and leaking it is not the problem. Row-level security is what makes a public key safe — which is exactly why a policy of using (true) matters so much. The service_role key is the opposite: it bypasses every policy and must never appear in client code.

I filter by user id in my frontend code. Is that enough?

No. A frontend filter decides what your app displays, not what the database will hand over. Anyone can open the network tab, take the public key and the endpoint, and ask for whatever they like. The filter has to live in the database.

Why test against staging rather than production?

Because a real test writes rows, changes them and deletes them. On production that means touching real customers' data. Point it at a copy.

Related: