← All case files

Case 006 · App built with an AI builder · Data access

The AI’s fix for “permission denied” opened every invoice.

An AI-written security policy cleared the error by letting every user read every invoice.

A dark monitor showing a code editor: “-- fix permission denied”, “create policy on invoices” and “using (true);” on a red line.
Before
Any user can read every invoice.
After
Policy scoped to the owner; two-user test in CI.

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.

invoices-rls.spec.tsIllustrative
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.

Tell us what you’re shipping.

An app, a rewrite, an AI agent, an automation, or a release that needs testing. A free 30-minute call, then a price in writing. If you don’t need us yet, we’ll say so.

Book a free call