Home / RLS / Update or delete does nothing (no error)
Supabase RLS · Error fix
Supabase update or delete does nothing, and there's no error
With RLS on, an UPDATE or DELETE only touches rows a policy lets you act on. No matching policy means 0 rows affected and no error. Add an UPDATE policy (with using and with check) and a DELETE policy, and keep a SELECT policy: UPDATE needs one too.
01 Why it's silent
RLS filters rows, it doesn't reject statements. An update ... where id = 1 that no policy allows simply matches nothing, so Postgres reports UPDATE 0 and supabase-js returns error: null. Inserts are different: a blocked insert raises new row violates row-level security policy.
Make the silence visible by asking for the affected rows back:
debug.ts
const { data, error } = await supabase .from('notes').update({ body }).eq('id', id).select(); console.log(data); // [] → RLS let you touch 0 rows
02 The usual causes
Only a SELECT policy exists
RLS is default-deny per operation. A read policy doesn't allow updates or deletes.
An UPDATE policy but no SELECT policy
To update a row Postgres must first find it, and that read goes through your SELECT policies. With no SELECT policy for the role, the UPDATE matches 0 rows.
No session
Called as anon, auth.uid() is null, so an owner policy matches nothing.
03 The fix
migration.sql
create policy "owner reads" on notes for select to authenticated using (user_id = (select auth.uid())); create policy "owner updates" on notes for update to authenticated using (user_id = (select auth.uid())) -- which rows you may change with check (user_id = (select auth.uid())); -- what they may become create policy "owner deletes" on notes for delete to authenticated using (user_id = (select auth.uid())); grant select, update, delete on notes to authenticated;
The with check on UPDATE matters: without it, a user could change user_id and hand their row to someone else.
04 Prove it
user_id to B → blocked by WITH CHECKCatch 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 in CI against a database with your migrations applied, before it reaches prod.
Or start from nextjs-supabase-starter : auth + a table with RLS + an isolation test, so you start from a pattern that's already sealed.
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
Why does the update work in the SQL editor?
The SQL editor bypasses RLS. Your app runs as authenticated or anon, where policies apply. Test writes the way the app makes them.
Can I get an error instead of 0 rows?
Not from RLS itself. Chain .select() after update() or delete() and treat an empty array as a failure in your code.