Home / RLS / Fix: new row violates row-level security policy
Supabase RLS · Error fix
Fix: new row violates row-level security policy
Your INSERT is blocked because the new row fails the table's WITH CHECK — usually because there's no INSERT policy, or the row's user_id doesn't equal auth.uid(). Add an INSERT policy with with check (user_id = (select auth.uid())) and make sure the row carries the signed-in user's id.
01 Why you're seeing this
Postgres throws new row violates row-level security policy for table "…" when Row-Level Security is on and the row you're writing isn't allowed by any policy's WITH CHECK expression. RLS on with no matching policy means deny — the safe default doing its job, not a bug. It fires on INSERT and on UPDATE (when the updated row would move outside what you're allowed to write).
There's no INSERT policy at all
You enabled RLS and added a SELECT policy, but never one for INSERT. Reads work, writes are denied — RLS is default-deny per operation.
The row's user_id doesn't match auth.uid()
You have with check (user_id = auth.uid()), but the client inserted the row without setting user_id (so it's null), or set a different id. The check evaluates false → violation.
The request isn't authenticated
If the call runs with the anon key and no signed-in session, auth.uid() is null, so user_id = auth.uid() can never be true.
02 The fix
Give the table an INSERT policy that lets a signed-in user write their own rows, and make sure the row carries their id.
migration.sql
-- 1. RLS on (safe to run if it already is) alter table notes enable row level security; -- 2. a signed-in user may insert rows that belong to them create policy "owner inserts own notes" on notes for insert with check (user_id = (select auth.uid()));
Let the database fill the id so the client can't get it wrong:
migration.sql
alter table notes alter column user_id set default (select auth.uid());
insert.ts
// with the default above, just don't send user_id await supabase.from('notes').insert({ title }); // or set it explicitly from the signed-in user const { data: { user } } = await supabase.auth.getUser(); await supabase.from('notes').insert({ title, user_id: user.id });
If cause 3 is yours, the fix is upstream: create the Supabase client with the user's session (their JWT), not the bare anon key — on the server, forward the request cookies so auth.uid() resolves.
03 The gotcha — USING vs WITH CHECK
USINGfilters rows the user can see or affect —SELECT,UPDATE,DELETE.WITH CHECKvalidates rows the user is writing —INSERTandUPDATE.
An INSERT policy with only USING and no WITH CHECK doesn't permit the write — that throws this exact violation. For UPDATE you usually need both: USING to pick the row, WITH CHECK so the user can't edit it into someone else's.
04 Prove it — don't hope
A policy you didn't test is a policy you don't have. Sign in as tenant A, confirm you can write your own row, and confirm you can't write one for tenant B.
Run that against a real Postgres in CI on every migration, so a table that ships without a correct write policy turns the build red before production.
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
Does this error mean my table is exposed?
No — the opposite. RLS is on and doing its job (deny by default). The risk is when RLS is off: then there's no error and the table is public. This error means the guardrail is working; you just need a policy that permits the legitimate write.
Why does the insert work in the SQL editor but fail from my app?
The SQL editor runs as a privileged role that bypasses RLS. Your app runs as authenticated or anon, which RLS applies to. Always test writes the way your app makes them — signed in, through the client.
Can I just disable RLS to make it go away?
You can, and you'll have made the table public to anyone with your anon key — the #1 Supabase leak. Keep RLS on and add the correct policy instead.