Home / RLS / Test tenant isolation
Supabase RLS · Testing
How to test tenant isolation in Supabase
Sign in as two real users, have B create a row, then assert A reads 0 rows for it and can't insert a row for B. Run it against a real Postgres in CI — a policy you didn't test is a policy you don't have.
01 The only proof that holds
You can't eyeball a policy and know it's correct. The proof is behavioral: sign in as tenant A, and confirm A can reach A's rows and cannot reach or write B's. Do it through the client, as authenticated — the SQL editor bypasses RLS, so testing there proves nothing.
02 The test (two signed-in clients)
rls.test.ts
import { createClient } from '@supabase/supabase-js' const signIn = async (email) => { const c = createClient(URL, ANON_KEY) await c.auth.signInWithPassword({ email, password: 'test-pw' }) return c } test('A cannot read B rows', async () => { const a = await signIn('a@test.dev') const b = await signIn('b@test.dev') const { data: row } = await b.from('notes') .insert({ title: 'B secret' }).select().single() const { data } = await a.from('notes').select().eq('id', row.id) expect(data).toHaveLength(0) // blocked, not just absent }) test('A cannot insert for B', async () => { const a = await signIn('a@test.dev') const { error } = await a.from('notes') .insert({ title: 'x', user_id: B_ID }) expect(error).toBeTruthy() // new row violates RLS })
03 What to assert
04 Run it in CI
Point the test at a real Supabase/Postgres (a CI branch database or a local supabase start) and run it on every migration. A new table that ships without a correct policy should turn the build red. To catch the coarser failure — a table with RLS off entirely — add an RLS gate in CI.
Catch this before it ships
airlock-rls is a CI gate that fails your build when a table ships exposed or a policy is permissive — the same class of bug, caught on the pull request instead of in prod.
Or start from nextjs-supabase-starter — auth + a table with RLS + an isolation test, so a fresh table is safe by default.
Grab the Supabase RLS cheat sheet
The golden rules, the footguns that leak in prod, correct policy snippets, and the isolation test — on one page.
FAQ
Should I use pgTAP instead of a JS test?
pgTAP works and runs in the database, but a JS integration test with two real signed-in clients tests the exact path your app uses — auth, the anon key, and the policies together. That end-to-end realism is worth a lot; use whichever your team will actually keep green.
Why assert zero rows instead of an error on read?
RLS filters reads silently — a blocked SELECT returns an empty set, not an error. So the correct assertion for reads is length 0. Writes, by contrast, raise the row-level-security violation you can assert on directly.