Guides by TICWays to earnPlatformsReality checkFree toolsRecipe Pack

Answers

new row violates row-level security policy for table "todos"

checked 2026-10-09

Short answer. This error means Row Level Security is on and no insert policy accepted the new row. There are four usual causes: no insert policy, an owner column that does not match the logged-in user, a returned row with no select policy, or nobody logged in. Each has its own fix below, tested on 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:

  1. There is no insert policy at all. With RLS on and no policy, the answer is no.
  2. The row's owner column does not match the logged-in user. A policy like with check (auth.uid() = user_id) rejects a row whose user_id is someone else's, or is missing.
  3. 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.
  4. 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.

Keep reading