The bug
An invoicing app built with an AI builder keeps its data in Supabase. Users who open their invoices get “permission denied”. The founder pastes the error into the AI builder, which writes a new row-level security policy, and the error goes away. The policy’s condition is using (true): every row, for everyone. Depending on the role it applies to, that means every logged-in user or anyone holding the app’s public key.
How it shows up
It looks like a fix. The invoices load, the error is gone and the founder moves on. Inside the app each user still sees only their own invoices, because that is all the app’s screens ask for. Anyone who queries the database directly, with the key the browser already has, gets everyone’s. In the project history, the change reads as a bug fix.
How a test finds it
Seed two test users with an invoice each. Sign in as user A and query invoices through the public API, the way the browser does, without the app’s filter. Count the rows that belong to B. Anything above zero fails. A second check reads the database’s list of policies and flags any policy on a user-data table whose condition is simply true.
The fix
Replace the policy with one scoped to the owner: auth.uid() = user_id. Then find the real cause of the original error. Usually it’s a row inserted without a user ID, which the owner policy had no way to match.
The test that catches it
A two-user test: user A requests user B’s invoices through the public API with the anon key and A’s token, and asserts zero rows. A CI query on pg_policies fails the build if any policy on a table holding user data has a condition of just true.
import { test, expect } from "@playwright/test";
import { anonClientSignedInAs, seed, sql } from "./db";
test("user A reads none of user B's invoices", async () => {
const supabase = await anonClientSignedInAs(seed.userA);
const { data } = await supabase
.from("invoices")
.select("id")
.eq("user_id", seed.userB.id);
expect(data ?? []).toHaveLength(0);
});
test("no policy on a user-data table is just `true`", async () => {
const open = await sql`
select tablename, policyname from pg_policies
where schemaname = 'public' and qual = 'true'`;
expect(open).toEqual([]);
});
What it puts at risk
Every customer’s invoices, with names, addresses and amounts, readable by any user of the app. It is a GDPR breach, and nothing in the app shows it.
