auditmyapp

Notes / Supabase errors

Fixing "new row violates row-level security policy" without opening your data

Most Supabase builders hit this error in their first week. The fix that makes it disappear fastest is the one that causes the most damage later.

This is what Supabase says when row-level security stops a row from being written:

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

It usually arrives with the error code 42501. The good news is that it is almost always telling you something specific and fixable. There are five common causes, and each one produces exactly this message. I reproduced all five on a real Postgres database while writing this.

The fix to avoid

Ask an AI assistant about this error and there is a good chance it suggests one of three things: a policy whose condition is simply true, switching row-level security off for the table, or using your secret key in the app. All three make the error go away. All three do it by letting everyone write to the table, which is the hole described in this note.

First, find out who Postgres thinks you are

Row-level security decides based on the role making the request. A signed-in user runs as authenticated, and a visitor with no session runs as anon. You can test an insert as a specific user in the SQL Editor, inside a transaction that undoes itself:

begin;
set local role authenticated;
select set_config('request.jwt.claims',
  '{"sub":"PASTE-A-REAL-USER-ID","role":"authenticated"}', true);

insert into public.notes (body) values ('test');

rollback;

Take a user ID from Authentication in the dashboard. If the insert fails here, the problem is in your policies. If it works here but fails in your app, the problem is in what your app sends.

The five causes

1. There is no insert policy

With row-level security on, anything not explicitly allowed is refused. A table with a select policy but no insert policy refuses every insert. Add one that ties the new row to the person creating it:

create policy "users insert their own notes"
  on public.notes for insert to authenticated
  with check ((select auth.uid()) = user_id);

2. The row doesn't carry the user's ID

That policy only passes if user_id matches the signed-in user. If your app doesn't send it, or sends a different one, the check fails. The cleanest fix is to let the database fill it in, so the app can't get it wrong:

alter table public.notes alter column user_id set default auth.uid();

It also stops a user creating rows that belong to someone else, which is exactly what the policy is for. In my test, the same insert carrying another user's ID was refused.

3. You are reading the row back without permission to read it

This is the one that confuses people most. In Supabase, .insert(...).select() writes the row and returns it in one step, and returning it requires the row to pass your select policy as well. Without one, you get the same error even though your insert policy is fine:

const { data, error } = await supabase
  .from('notes')
  .insert({ body: 'hello' })
  .select();   // this part needs a select policy too

Either add a select policy that lets users read their own rows, or drop the .select() if you don't need the row back.

4. It is an upsert, and there is no update policy

An upsert inserts a row, or updates it if it already exists. When it exists, Postgres needs an update policy too. You can recognise this case by a slightly different message:

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

"USING expression" points at the update side. Add an update policy with the same ownership condition in both using and with check.

5. Nobody is signed in when the request runs

If the request goes out before the session has loaded, or from server code that doesn't pass the user's session, it runs as anon. Your policy for signed-in users doesn't apply, so the insert is refused. Make sure the session is ready before writing, and that server-side calls include the user's access token.

Storage uploads throw it too

Uploading a file writes a row into storage.objects, so a missing storage policy produces the same error. Uploads need an insert policy on storage.objects, and if you upload with upsert: true, select and update policies as well. The self-check note includes a storage policy that limits each user to a folder named after their own ID.

Why the quick fix is worse than it looks

When I added a true insert policy for signed-out users in my test, the next insert still failed, this time because user_id was empty and the column required it. On a column that allowed empty values, the same "fix" would have quietly created rows with no owner, writable by anyone who found the table. The error was protecting you.

If you have already shipped one of the quick fixes, the free self-check will find it in about a minute.