Home / RLS / Owner-scoped RLS policy
Supabase RLS · Policy pattern
How to write an owner-scoped RLS policy
Store user_id on the table and compare it to (select auth.uid()) in one policy per operation: USING for SELECT/DELETE, WITH CHECK for INSERT, and both for UPDATE. Full set below.
01 The rule
Owner-scoped means: a row belongs to one user, and only that user can touch it. Store the owner as user_id and compare it to (select auth.uid()) — the id of the signed-in caller — in every policy. Wrapping it in a subselect lets Postgres cache the value per statement, a real speed win at scale.
02 The full policy set
One policy per operation. Note where each uses USING vs WITH CHECK:
migration.sql
alter table notes enable row level security; -- read: filter to rows you own create policy "select own" on notes for select using (user_id = (select auth.uid())); -- create: the new row must belong to you create policy "insert own" on notes for insert with check (user_id = (select auth.uid())); -- update: you can only touch your rows (USING) and can't reassign them (WITH CHECK) create policy "update own" on notes for update using (user_id = (select auth.uid())) with check (user_id = (select auth.uid())); -- delete: you can only delete your rows create policy "delete own" on notes for delete using (user_id = (select auth.uid()));
02b Why UPDATE needs both
USING decides which rows you may update; WITH CHECK validates the row after your change. Without WITH CHECK, a user could update their own row and set user_id to someone else's — handing it away. Both clauses close that.
03 Prove it
user_idSee how to test tenant isolation to turn that into an automated check.
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
Can I use one policy for all operations?
You can write a policy FOR ALL, but you lose the USING/WITH CHECK distinction that matters for writes. A policy per operation is clearer and safer — especially so UPDATE gets both clauses.
Where does user_id come from on insert?
Set a column default of (select auth.uid()) so the database fills it, or set it explicitly from the signed-in user on the client. Either way the WITH CHECK guarantees it matches the caller.