New row violates row-level security policy

Postgres refused to save a row because no row-level security policy on that table lets the current user write it.

Supabase and browser console
new row violates row-level security policy for table "todos"

Also appears as

  • new row violates row-level security policy (USING expression) for table "todos"

Prompt to fix it

Saving data fails with "new row violates row-level security policy". Keep row-level security on. Find the table named in the error, read its current policies and the code that writes to it, and add the missing policies so signed-in users can read, insert, and update only their own rows (auth.uid() = user_id). Make the insert set user_id from the current session, or default the column to auth.uid(). If the code uses upsert or .select() after the write, cover those too. Show me the SQL before you run it.

Paste into Layerwise or any AI coding tool.

What it means

Row-level security (RLS) lets a Postgres table decide, row by row, who can read and change data. Supabase turns it on for tables created in the Table Editor, and once it’s on, every action is denied until a policy allows it. This error means an insert, update, or upsert reached the table and no policy accepted the new row.

Reads fail more quietly. A select that RLS blocks returns an empty list with no error, which is why a page can load fine and still show nothing.

The real fix is a policy that matches how the app writes data, usually “signed-in users can write rows where user_id is their own ID.” Turning RLS off also makes the error go away, but then anyone holding your public anon key can read and change every row.

Common causes

  • RLS is on and the table has no INSERT or UPDATE policy yet.
  • The policy checks auth.uid() = user_id, but the insert leaves user_id empty or sets it to a different ID.
  • The request went out without the user’s session, so auth.uid() is null and the policy for authenticated users never applies.
  • The code calls upsert, which needs both an INSERT and an UPDATE policy, or chains .select(), which also needs a SELECT policy.

How to fix it

  1. In the Supabase dashboard, open the table named in the error and review its policies.
  2. Add an INSERT policy for the authenticated role with WITH CHECK (auth.uid() = user_id), and an UPDATE policy with the same check in USING and WITH CHECK.
  3. Make the insert send user_id from the signed-in session, or give the column a default of auth.uid().
  4. Call supabase.auth.getUser() right before the write and confirm it returns a user.
  5. If the code uses upsert or .select() after the write, add the matching UPDATE and SELECT policies.

Example fix

-- Signed-in users can manage only their own rows.
alter table todos enable row level security;
-- Newer Supabase projects don't grant API access to new tables on their own.
grant select, insert, update on todos to authenticated;

create policy "Read own todos" on todos
for select to authenticated
using ((select auth.uid()) = user_id);

create policy "Add own todos" on todos
for insert to authenticated
with check ((select auth.uid()) = user_id);

create policy "Update own todos" on todos
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

-- Fill user_id when the app leaves it out.
alter table todos alter column user_id set default auth.uid();
All errors
infinite recursion detected in policy for relation "profiles"

Infinite recursion detected in policy for relation

A row-level security policy reads from its own table, so checking the policy runs the same policy again, without end.

permission denied for table todos

Permission denied for table

The database role making the request has no grant on that table, so Postgres refuses before row-level security even runs.

AuthSessionMissingError: Auth session missing!

AuthSessionMissingError: Auth session missing

A Supabase auth call needed a signed-in user, but the client making it had no session, often because it runs on the server without cookies.

relation "todos" does not exist

Relation does not exist

The query names a table that Postgres can’t find in this database, usually because it was never created here or has a different name.

Build with an agent that checks its work

The Layerwise agent clicks through the live preview and reads the console, so it catches errors before you do.