DEV Community

Tori TIC
Tori TIC

Posted on Originally published at ticassociation.com

Supabase 42501: new row violates row-level security

PostgreSQL refused the write because no policy let this user write this row. In a Supabase app that comes down to one of six causes, and three things tell you which: the table named in the error, the role making the request, and whether you asked for the row back.

What you are looking at

{
  "code": "42501",
  "message": "new row violates row-level security policy for table \"profiles\""
}
Enter fullscreen mode Exit fullscreen mode

What is error 42501? 42501 is the PostgreSQL error code insufficient_privilege. PostgreSQL raises exactly this message, with exactly this code, when a new row fails a row level security check; you can see it in its executor's source. The same code also covers permission denied for table, which is a missing grant rather than a policy. That case is further down.

Six causes. Check them in this order

There is no insert policy for your role

With row level security on, PostgreSQL refuses anything no policy allows. In its own words: "If no policy exists for the table, a default-deny policy is used, meaning that no rows are visible or can be modified." Check the table's policies in your dashboard: is there an INSERT (or ALL) policy for the authenticated role? If not, add one:

create policy "insert own rows"
on public.profiles for insert to authenticated
with check ((select auth.uid()) = user_id);
Enter fullscreen mode Exit fullscreen mode

The row you send fails the check

The policy exists, but the new row does not pass its WITH CHECK. The usual reason is that the app never sends user_id, or sends a different one. PostgreSQL: "Rows being inserted that do not pass this policy will result in a policy violation error, and the entire INSERT command will be aborted." Send the signed-in user's id, or let the database fill it in so the app cannot get it wrong:

alter table public.profiles alter column user_id set default auth.uid();
Enter fullscreen mode Exit fullscreen mode

The request is not signed in

Your policy says to authenticated, but the request reached the database as the anonymous role: the client had no session yet, or a server route built its client without the user's session. Supabase: "Once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies." Check that a session exists right before the insert, and on the server build the client from the request's cookies. Do not widen the policy to anon to make the error go away.

You chained .select() after .insert()

Supabase's client returns nothing from an insert by default: "By default, inserted rows are not returned. To return it, chain the call with .select()." Asking for the row back adds a RETURNING clause, and then PostgreSQL also requires a SELECT policy the new row passes: "If a newly inserted or updated row does not satisfy the relation's SELECT policies, an error will be thrown." That is why the same insert works without .select() and fails with it. Add a select policy, or drop .select():

create policy "select own rows"
on public.profiles for select to authenticated
using ((select auth.uid()) = user_id);
Enter fullscreen mode Exit fullscreen mode

It is an upsert

An upsert is an insert that may turn into an update, so it needs an update policy as well as an insert policy. When the row already exists, PostgreSQL checks it against your update policies and, as its documentation says, "unlike a standalone UPDATE command, if the existing row does not pass the USING expressions, an error will be thrown." It needs a select policy too: the same page says "the rows proposed for insertion are checked using the relation's SELECT policies", and on a real PostgreSQL an upsert of a new row with no select policy was refused with 42501. Add the update policy:

create policy "update own rows"
on public.profiles for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
Enter fullscreen mode Exit fullscreen mode

It is a file upload

If the table in the error is objects, the refusal came from Supabase Storage, not your own table. Supabase: "By default Storage does not allow any uploads to buckets without RLS policies. You selectively allow certain operations by creating RLS policies on the storage.objects table." This lets signed-in users upload into one bucket; overwriting with upsert also needs SELECT and UPDATE policies. The four upload causes, a folder-scoped policy and the upsert policies are on the Storage upload page:

create policy "signed-in users can upload"
on storage.objects for insert to authenticated
with check (bucket_id = 'your-bucket');
Enter fullscreen mode Exit fullscreen mode

Two fixes that trade the error for a leak

Do not switch row level security off. The error goes away because nothing is checked any more. Supabase gives its API roles access to tables in the public schema by default, so without RLS anyone holding your public key can read and write the table.

Do not move the insert to the secret key in the browser. Supabase: "A secret key authorizes access through the service_role Postgres role, which has the bypassrls attribute. Never use a secret key in the browser or expose it to customers."

Same code, different cause

If the message is permission denied for table, no policy was ever consulted: the role has no grant on the table at all. Supabase's own troubleshooting page explains that by default "tables in the public schema are granted SELECT, INSERT, UPDATE, and DELETE to the anon and authenticated roles". A table in a custom schema, or one whose grants were revoked, has to be granted again. The auth and vault schemas are closed to the API on purpose, so reach them through a security definer function instead.

grant select, insert, update, delete on table public.profiles to anon, authenticated;
Enter fullscreen mode Exit fullscreen mode

Straight answers

Why does the same insert work in the SQL editor?

Because the dashboard runs your queries as the postgres role (Supabase: "Queries you run in the Dashboard execute as postgres"), the role that created, and so owns, your tables. PostgreSQL lets table owners skip row security: "Table owners normally bypass row security as well." Your users are not table owners, so the same insert is checked for them. When the app's request carries no session at all, auth.uid() is null and every ownership check fails; that case has its own page.

What does error 42501 mean in Supabase?

42501 is PostgreSQL's code for insufficient_privilege. With the message "new row violates row-level security policy", a row you tried to write failed a row level security check. With "permission denied for table", the role has no grant on the table.

Is it safe to just turn RLS off?

No. Supabase gives its API roles access to tables in the public schema by default, so a table without row level security is readable and writable by anyone holding your public key. Fix the policy instead.

One broken policy is rarely alone. Check every table

Our free auditor reads your policies and lists the holes worst first, including the quiet ones that never raise an error. It runs in your own SQL editor in about a minute and never touches your rows.

Get the free RLS auditor

Also seeing 42P17, infinite recursion detected in policy?

© 2026 TIC Association Privacy · Terms hello@ticassociation.com


Originally published with the full SQL and sources at https://ticassociation.com/supabase-42501-row-level-security.

Top comments (0)