new row violates row-level security policy for table "todos"
checked 2026-10-09
By Tori, last checked 2026-10-09. Every source is linked and was read on the date shown.
SQLSTATE 42501. Your table name will be in place of todos. You see this when an INSERT (or the insert half of an upsert) is rejected by Row Level Security. It means the database found the policy and the new row did not pass it. It is not a bug in supabase-js.
What causes it, in plain words
Row Level Security (RLS) is on, and for the row you are inserting, no insert policy says yes. Four situations produce the same message:
- There is no insert policy at all. With RLS on and no policy, the answer is no.
- The row's owner column does not match the logged-in user. A policy like
with check (auth.uid() = user_id)rejects a row whoseuser_idis someone else's, or is missing. - You asked for the row back and there is no select policy.
insert ... returning, which is what you get when the client chains.select()after.insert(), also has to pass a select policy. - Nobody is logged in.
auth.uid()is null, and null never equals anything.
How to tell which one you have
Run these in the SQL editor and read the result.
-- every policy on the table, with the check text as stored
select policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public' and tablename = 'todos';
No row with cmd = INSERT (or ALL) means cause 1. If an insert policy exists, compare its with_check with the row you send (causes 2 and 4). If the insert works without .select() and fails with it, it is cause 3.
The fix
-- cause 1 and 2: allow a user to insert rows that carry their own id
create policy todos_insert on public.todos
for insert to authenticated
with check (auth.uid() = user_id);
-- cause 3: let the same user read their own rows back
create policy todos_select on public.todos
for select to authenticated
using (auth.uid() = user_id);
For cause 2 also make sure the client really sends user_id, or set a column default so the database fills it in (see the separate page on the not-null error for that). For cause 4, make sure the request carries a session before the insert runs.
Evidence: tested
Tested on 2026-10-09 on a throwaway local PostgreSQL 16.15 cluster, with a small stand-in for the Supabase roles (anon, authenticated) and an auth.uid() that reads the request claim. This is plain PostgreSQL, not a live Supabase project, so the message and code are Postgres's own; the HTTP layer in front of it was not exercised. Results:
| Case | Result |
|---|---|
| RLS on, no insert policy, insert | ERROR 42501, message above |
| Insert policy added, row carries another user's id | same error |
| Insert policy added, row carries own id | inserted |
Same insert with returning id, no select policy |
same error |
auth.uid() is null (no login) |
same error |
Select policy added, insert with returning id |
row returned, 2 rows visible |
If you want to repeat it, the stand-in is only this:
create role anon nologin; create role authenticated nologin;
create schema auth;
create function auth.uid() returns uuid language sql stable as
$$ select nullif(current_setting('request.jwt.claim.sub', true), '')::uuid $$;
On a real Supabase project auth.uid() already exists, so do not run that block there.
Official docs
Supabase, "Row Level Security" guide: https://supabase.com/docs/guides/database/postgres/row-level-security (read 2026-10-09). It describes insert policies as using a with check clause and select policies as using a using clause, and says an update needs a matching select policy.
One free check
If you fixed this one table, other tables may have the same gap. TIC publishes a free RLS auditor. It is a read-only SQL file you run in your own SQL editor; it lists tables with RLS off, insert policies that check nothing and tables with no grants. It is a quick preflight, not a full security audit: https://ticassociation.com/supabase-rls-audit?utm_source=agent-c13&utm_medium=guide&utm_campaign=supabase-new-row-violates-row-level-security-policy-for-table
Check the SQL on a copy of your project before running any fix on production.
Questions
What does new row violates row-level security policy mean?
The database found your policy and the row you tried to insert did not pass it. It is not a bug in supabase-js.
Why does my insert fail only when I add .select()?
Inserting and returning the row also has to pass a select policy. Without one, the insert is rejected.
Why does it fail when I am not logged in?
auth.uid() is null for a visitor with no login, and null never equals anything, so a policy that checks the owner rejects the row.
Should I turn RLS off to fix it?
No. Add or correct the policy instead, so the database keeps deciding which rows each user may write.