PostgreSQL · 42501
new row violates row-level security policy — cause and fix (Supabase)
Luca Urti
new row violates row-level security policy
Why does my INSERT fail with “new row violates row-level security policy”?
The table has Row Level Security enabled and no policy permits this particular row to be inserted by this particular role. PostgreSQL checks an INSERT against the with check expression of every policy whose command covers INSERT, and there is one rule people consistently get backwards: if a policy omits with check, PostgreSQL uses that policy's using expression as the check instead. So for all using (auth.uid() = user_id) does govern inserts — it permits rows you own and rejects rows you do not — while a policy declared for select covers no insert at all, no matter what its expression says. The error is PostgreSQL refusing to write, not Supabase misconfiguring anything, and it means RLS is working.
The three shapes this takes
Case one, RLS on with no policy at all. The most common one, and it appears the moment you run enable row level security on a table your app writes to. Enabling RLS denies everything until a policy allows something — the insert raises this error while a select on the same table quietly returns an empty list.
Case two, policies exist but none of them covers INSERT. A table whose only policy is create policy "read own" on public.notes for select using (auth.uid() = user_id); rejects every insert, because the command a policy is declared for is what decides whether it is consulted at all. for select and for update policies are never applied to an insert.
Case three, an insert-capable policy exists and this row fails its check. This is where the using-doubles-as-with check rule matters: for all using (auth.uid() = user_id) accepts a row whose user_id is your own and rejects one belonging to somebody else, using the same expression for both jobs. It also rejects everything when the request carried no session, because auth.uid() is then null and null = anything is not true — which is why this error often means “no session”, not “bad policy”.
Check which one you have before writing a policy
PostgreSQL names the table in the full message — new row violates row-level security policy for table "notes" — so start by reading which table it is, because in a multi-table insert or a trigger it is often not the one you called.
Then run this in the SQL editor. No rows at all means case one. Rows, but none whose cmd is INSERT or ALL, means case two. If such a row exists, you are in case three, and the expression to read is its with_check — or, when that column is null, its qual, because that is the one PostgreSQL falls back to. At that point the problem is the value your client is sending or the session it is sending it with, not the policy.
Setting user_id from the client is the part people get wrong
The correct pattern is not to send user_id from the browser at all. Default it in the database to auth.uid() and let the policy check it. A client that sends its own user_id and a policy that checks it is fine — the policy will catch a forged value — but a column with no default and a client that forgets to set it produces exactly this error, and the usual reaction is to loosen the policy rather than fix the column.
The fix
diagnose, then fix
-- 1. What policies does this table actually have? select policyname, cmd, qual as using_expr, with_check from pg_policies where schemaname = 'public' and tablename = 'notes'; -- 2. The missing INSERT policy, with the column defaulted server-side alter table public.notes alter column user_id set default auth.uid(); create policy "insert own notes" on public.notes for insert to authenticated with check (auth.uid() = user_id);
The fix that works and costs you the database
The fix that removes the error and your access control at the same time is switching the client from the anon key to the service_role key, because service_role bypasses RLS entirely and every insert starts working immediately. That key is a full-database credential. In a browser bundle, a Next.js NEXT_PUBLIC_ variable or a React Native app it is readable by anyone who opens devtools, and it grants read and write on every table you have, not just this one. The second version of the same trap is with check (true), which keeps the policy and removes what it checked.
Whether the trap is already in your repo is a question you can answer
Sentris reads the SQL and the client code, so it reports the shortcut above where it was actually taken — a service_role key in a browser bundle, a policy that is using (true), a table with RLS switched off. A scan needs no account and no card. How often it is wrong is measured and published.