Three causes cover almost every case of new row violates row-level security policy in Supabase. One, RLS is on and there is no INSERT policy. Two, the request never carried a session, so auth.uid() is null and the policy compares against nothing. Three, the request also reads or replaces a row, so SELECT or UPDATE access is missing. The SQL for each is below.

Checked against supabase-js v2 and the current Supabase docs on 5 August 2026.

new row violates row-level security policy means Postgres rejected the row against an applicable RLS check. The accompanying SQLSTATE is often 42501, which means insufficient privilege, but 42501 is broader than RLS: Supabase also documents missing table grants, column restrictions, and forbidden schemas under that code. Read the full message, detail, and hint before changing a policy. Turning RLS off can clear the symptom by removing the check and expose the table through the Data API. For the concept underneath the fixes, what a policy decides on each operation is the concept guide on this site.

If you came here on a general question about what row-level security in Supabase is, rather than a write that just failed, the concept guide on this site covers the model itself: what RLS does, how a policy attaches to a table, and when Supabase turns it on for you. This page stays on the error.

Cause 1: RLS is on and there is no INSERT policy

If you enabled RLS on a table and never wrote an INSERT policy, inserts through Supabase’s Data API fail for anon and authenticated requests. The SQL editor or another table-owner connection can behave differently because table owners normally bypass RLS. For user-owned rows, start with an INSERT policy whose with check clause ties the new row to the authenticated user.

You do not need the SQL editor for this. In the Supabase dashboard, open Table Editor and select the table to confirm RLS is on (tables created in the Table Editor have RLS enabled by default, tables created in raw SQL do not). Then go to Database, then Policies, pick the table, and choose New Policy. Set the operation to INSERT, pick the authenticated role, and put the ownership rule in the WITH CHECK box. The expression is the same one in the SQL below.

create policy "Users can insert their own rows"
on orders
for insert
to authenticated
with check ( (select auth.uid()) = user_id );

That policy matches Supabase’s current row-level security guide: it names the authenticated role and checks that user_id matches the identity in the request. It is an example, not a universal policy. A shared-resource table, admin workflow, or server-owned row needs a rule that matches its actual ownership model.

What error 42501 means

42501 is Postgres’s insufficient_privilege code. Supabase’s current 42501 troubleshooting guide separates RLS failures from forbidden schemas, custom-schema configuration, missing table privileges, and column-level restrictions. If the message specifically says a new row violates an RLS policy, use the six RLS checks below. If it says permission denied for table, inspect grants instead of weakening the row policy.

SymptomLikely causeFix
Every app insert fails, while an owner connection worksRLS is on and no applicable INSERT policy existsAdd an INSERT policy with a real with check
The request reaches the anon role instead of authenticatedThe access token is missing, expired, or not attachedFix the session/JWT path; do not broaden the policy by accident
Insert fails right after login, or only for logged-out usersauth.uid() is returning null because no session reached PostgresConfirm the JWT is attached to the request; guard with is not null
Insert fails on one column’s value but the row looks right otherwisewith check compares against a column that isn’t the real owner idPoint the check at the column that actually holds the caller’s id
The plain insert works but .insert(...).select() failsThe returned representation is subject to read visibilityAdd a matching SELECT policy if the caller should read the row, or do not request it back
A Storage upload fails with the same messageStorage applies policies on storage.objects; current Supabase pages disagree on whether an ordinary upload also reads the row backStart with INSERT, then test whether this request also needs SELECT or UPDATE (see the Storage section below)
New row violates row level security policy router from six symptoms to first fixes

The table name in the message does not change the fix

Most people arrive here after searching the message with their own table name inside it, such as for table "orders". Postgres includes that name so you know where to look. For an ordinary application table, the name varies by schema and does not tell you which cause applies.

So the path is the same whatever sits in the quotes. Put your table name into the pg_policies query further down, read what comes back, then work through the causes on this page in order.

Two table names do change what you fix, and both have their own section below. storage.objects is where Supabase Storage keeps its policies, so a file upload fails against that table and never against your application table. And a profile row written during signup, in profiles or user_profiles, fails for a timing reason rather than a policy-syntax one.

Storage uploads: 403 Forbidden, new row violates row-level security policy

If the error arrives from a file upload rather than a table insert, the client usually shows it as a StorageApiError with statusCode: "403", error: "Unauthorized", and the message new row violates row-level security policy. You may also see it written as a plain 403 Forbidden. Same Postgres check, different surface.

Supabase’s current pages give different instructions for an ordinary upload. The Storage access-control guide says an ordinary upload requires only INSERT. It says an upsert additionally requires SELECT and UPDATE. The upload troubleshooting page says the Storage API can use INSERT ... RETURNING *, which fails when the response needs metadata that no SELECT policy allows.

Start with the narrow INSERT policy below for an ordinary upload. If that exact upload still fails after the INSERT rule and JWT are confirmed, test the troubleshooting page’s returned-metadata condition. Add a matching SELECT rule only when the caller should read that object. For an upsert, add the documented SELECT and UPDATE permissions too.

-- Write the object.
create policy "Users can upload to their own folder"
on storage.objects
for insert
to authenticated
with check (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);

-- Add this only when this caller should read the object or the observed upload path returns its metadata.
create policy "Users can read their own objects"
on storage.objects
for select
to authenticated
using (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);

Bucket visibility matters here. A public bucket serves its files over a public URL, so fetching the public file does not need a SELECT policy. A private bucket needs policies for the operations the caller performs. The upload itself always needs INSERT on storage.objects. Add SELECT or UPDATE only for an operation that requires it.

The policies live in the database, so they apply from any client. The Python client hits the same rules:

with open("./avatar1.png", "rb") as f:
    response = (
        supabase.storage
        .from_("avatars")
        .upload(
            file=f,
            path=f"{user_id}/avatar1.png",  # first folder must equal the caller's auth.jwt() sub
            file_options={"cache-control": "3600", "upsert": "false"},
        )
    )

A JavaScript upload, a Python upload, and an upload fired from a no-code builder all fail the same way for the same reason. Fixing the client library will not fix a missing policy.

The insert fails right after signup (user_profiles and auth.users)

The other common version of this error is a profile row. You create an account, the app immediately inserts into user_profiles (or profiles) with a uuid that references auth.users, and that insert is rejected. If instead nobody is getting as far as an account at all, customers stuck at the login screen is the failure that looks like this one from the outside.

The reason is timing. At the moment the client fires that insert, the session often does not exist yet or has not been attached to the request, so the call runs as anon. A policy written to authenticated never applies, and auth.uid() is null, so an ownership check like auth.uid() = id compares null to a value and never matches. Waiting for the session in the client works, but it is fragile: it breaks for email-confirmation flows, where no session exists until the user clicks the link. A signup email that never arrives at all is the other error every new Supabase app hits, and it has its own page.

The standard fix is to stop writing the profile row from the client. Supabase documents a trigger on auth.users that writes the profile row server-side, in the same transaction as the signup. The function is declared security definer, so it runs with the privileges of its owner and is not blocked by the table’s RLS policies.

create table public.user_profiles (
  id uuid not null references auth.users on delete cascade,
  first_name text,
  last_name text,
  primary key (id)
);

alter table public.user_profiles enable row level security;

create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
begin
  insert into public.user_profiles (id, first_name, last_name)
  values (
    new.id,
    new.raw_user_meta_data ->> 'first_name',
    new.raw_user_meta_data ->> 'last_name'
  );
  return new;
end;
$$;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

Two cautions. security definer bypasses RLS by design, so keep the function tiny and let it write only the row for new.id. And Supabase warns that a failing trigger can block signups, so test the whole signup path after you add it. The profile row still needs its own SELECT and UPDATE policies for the user to read and edit it later.

USING vs WITH CHECK, and why an insert only checks one of them

Supabase’s own documentation draws the line cleanly: using decides which existing rows a policy lets you see or touch, and applies to select, update, and delete. with check validates the row you’re about to write, and applies to insert and update. An insert has no existing row to filter, so its policy only ever needs with check. Update needs both: using to confirm you can touch the row that’s there, with check to confirm the row still qualifies after your change.

The fourth diagnostic-table row, the one where the row “looks right” but still gets rejected, can be this clause pointed at the wrong column. A policy may read with check (true), which passes every row for the targeted role, or compare auth.uid() with id when the table’s ownership column is actually user_id. Postgres evaluates the expression exactly as written.

Cause 2: Wrong role, or auth.uid() returns null

Two of the six causes look identical from the client and come from opposite directions. The first is a policy written for a role the request never assumes. A Supabase browser client can still use the project’s publishable or legacy anon key after login; the user’s access token is what changes the database role from anon to authenticated. A missing or expired token leaves the request in anon, so a to authenticated policy never applies.

The second is a null identity reaching a policy that assumed there’d always be one. Supabase’s documentation states it directly: when a request carries no authenticated user, auth.uid() returns null, and null = user_id is never true in SQL, for any value of user_id. An expired session, a request that never attached its access token, a server-side call still running on the anonymous key: all three produce the same silent no-match. The documented fix is to check for the null explicitly rather than let it fail the comparison quietly:

with check ( (select auth.uid()) is not null and (select auth.uid()) = user_id )

That extra clause doesn’t change what the policy allows. It makes the authentication requirement explicit to the next person reading the policy.

Cause 3: The plain insert works, but .select() fails

Supabase’s JavaScript client does not return modified rows by default. Chaining .select() asks for the new row as a returned representation. If the plain insert succeeds and the combined .insert(...).select() request fails, inspect the SELECT policy that controls whether the caller can see that row. Treat the failed combined request as failed until you verify database state; do not tell the user the record was saved based only on the first half of the intended operation.

// First isolate the write path without requesting a representation.
const { error } = await supabase
  .from("orders")
  .insert({ user_id: userId, total });

// Then test the returned-row path separately.
const { data, error: selectError } = await supabase
  .from("orders")
  .insert({ user_id: userId, total })
  .select();

The most-copied fix online is { returning: 'minimal' } passed as a second argument to .insert(). That was supabase-js v1. Supabase’s own v2 upgrade guide says insert, upsert, update and delete no longer return rows by default, and that you add .select() when you want them back. In v2 the old option is not a supported way to silence this error and it will not stop it. Omit .select() when the caller does not need the row back. When it does, add a SELECT policy only if that caller should legitimately be able to read the row.

See which policies actually exist on the table

Most of the guesswork disappears once you list the policies Postgres has, rather than the ones you think you wrote. Run this in the SQL editor:

select policyname, cmd, roles, permissive, qual, with_check
from pg_policies
where tablename = 'orders';

Read it left to right. cmd is the operation the policy covers (INSERT, SELECT, ALL), roles is who it applies to, qual is the USING clause, and with_check is the WITH CHECK clause, which for an INSERT policy is the entire rule. If the query returns no rows at all and RLS is on, that is cause 1 above: there is no policy to satisfy.

permissive is the column people miss. Permissive policies OR together, so adding one more can only widen access. A row that reads RESTRICTIVE ANDs with everything else, so a single stray restrictive policy blocks your insert no matter how correct your new policy is. If you see one, read its expression before you write another policy.

Why you shouldn’t just disable RLS in Supabase to fix this

Three fixes make this error disappear without making the app safe, and I’ve seen the first two land in real audits, not hypotheticals.

One food-delivery app in the fixed AxonBuild research cohort had a notifications table added after the rest of the schema. RLS was never enabled. A trigger copied each order’s customer name, phone number, delivery address, GPS coordinates, and total into that table. Because the table sat in an exposed schema and the API roles had table privileges, the app’s public client credentials were enough to read it. Supabase’s API security guide treats grants and RLS as separate controls; both need to match the intended access.

Another app in the corpus shipped an INSERT policy on its questions table that read with check (true). That RLS check accepts every insert from the targeted role, logged in or not. using (true) does the equivalent for reads covered by that policy: the policy exists, but its expression does not restrict rows.

The third is a service_role key reaching client-side code, sometimes on purpose, as a shortcut around a policy nobody wanted to write correctly. That key was built to skip row-level security entirely, for server code you control. In a browser bundle it hands out the same bypass to anyone who opens dev tools, the exact failure mode behind the holes AI coding tools ship by default.

This is why the error is so common in apps built with Lovable, Bolt, Base44, v0, Replit, n8n, WeWeb and FlutterFlow. The generator creates the schema in one step and the access rules in another, and the two steps do not always meet. Two patterns show up from the app side. Either RLS is switched on with no matching policy, and every write from the app fails while the same statement succeeds in the SQL editor, which is the error you are reading about. Or RLS is left off entirely, nothing fails, and the table is readable by anyone holding the public client key, which is worse and silent. If a builder or an AI assistant “fixes” this error for you, check which of the two it did before you ship.

Looks fixed Actually fixed
RLS is disabled, and the insert works againRLS is on, with a with check clause tied to the caller
The policy reads with check (true)The policy compares the row to auth.uid(), not to true
The client uses the service_role key to skip the errorThe service_role key never leaves server code
Looks fixed
RLS is disabled, and the insert works again
The policy reads with check (true)
The client uses the service_role key to skip the error
Actually fixed
RLS is disabled, and the insert works again
RLS is on, with a with check clause tied to the caller
The policy reads with check (true)
The policy compares the row to auth.uid(), not to true
The client uses the service_role key to skip the error
The service_role key never leaves server code

Across the fixed June–July 2026 AxonBuild research cohort, RLS gaps appeared in 9 of 21 third-party apps, and 7 of 21 had a confirmed path where one logged-in customer could read or write another customer’s data. Those are selected-corpus findings, not a Supabase-wide failure rate.

A missing INSERT policy blocks the write. A policy that says with check (true) can admit every row from its targeted role.

Test that the policy actually protects you

A policy that compiles isn’t necessarily a policy that works. Test it with requests made under each relevant identity and role, then inspect the results rather than trusting the syntax.

  1. 01 Create two real accounts in your app, not two rows in the same session
  2. 02 Using the anon key with no session attached, try the insert you just wrote a policy for. It should fail
  3. 03 Log in as account A, insert a row, and confirm it succeeds and the returned data belongs to account A
  4. 04 Log in as account B, and try to insert a row using account A’s id in the owner column. It should fail even though B is authenticated
  5. 05 If you added .select(), confirm account B still cannot read a row that belongs to account A

A static check can tell you a policy exists. A request made under a second authenticated identity tests whether the policy actually separates one user’s rows from another’s.

Common questions about the Supabase RLS error

Do I need a SELECT policy just to insert a row?

For an ordinary table insert, not unless the request also asks Postgres to return the new row, which .select() does. An insert with no .select() chained does not need a SELECT policy. Supabase’s Storage access-control guide says an ordinary upload needs INSERT, while upsert also needs SELECT and UPDATE. Its upload troubleshooting page separately says some failed uploads need SELECT for returned metadata. Follow the operation you are performing and the failure you observed.

Why does the insert work in the SQL editor but fail in my app?

Because the two requests run as different Postgres roles. The SQL editor connects as a table owner, and table owners normally bypass row-level security, so no policy is evaluated. Your app connects as anon or authenticated through the Data API, where every policy applies. A statement that works in the editor tells you the SQL is valid, not that the policy is right.

What does the 403 Forbidden or Unauthorized error mean here?

It is the same rejection seen from the HTTP layer instead of the database. A Supabase Storage upload that fails a policy comes back as a StorageApiError with statusCode: "403" and error: "Unauthorized", carrying the message new row violates row-level security policy. Fix it with policies on storage.objects, not with client settings or a different upload library.

What is error 42501?

Postgres’s standard insufficient-privilege code. In Supabase it can point to an RLS rejection, a missing table privilege, a column restriction, or a forbidden schema. Read the full error message and hint before choosing the RLS troubleshooting path.

Is this error Supabase-specific, or plain Postgres?

Plain Postgres. Row-level security is a PostgreSQL feature and the rejection comes from the database itself: PostgreSQL’s CREATE POLICY documentation says rows being inserted that do not pass the policy “will result in a policy violation error, and the entire INSERT command will be aborted”. Any Postgres database with RLS enabled and no matching policy raises it, self-hosted or managed.

What Supabase adds is exposure. Its tables sit behind the Data API, so a missing INSERT policy shows up as a broken signup form in the browser instead of a server-side error nobody sees. That also makes the fix portable: the SQL on this page is standard create policy, and only the Supabase-specific pieces change if you run Postgres elsewhere, auth.uid() for the caller’s identity and storage.objects for file uploads. Client libraries wrap the message in their own error type, which is why the same line reaches people printed as something like postgres exception(message: new row violates row-level security policy for table ...).

Should I just disable RLS to make it stop?

Do not disable RLS as a debugging fix on a table exposed through Supabase’s Data API. For server-only data, use a private or unexposed schema and least-privilege grants as an explicit design choice. RLS, schema exposure, and table grants are separate controls.

Why is auth.uid() null when I’m logged in?

Usually because the request that hit Postgres never carried your session: an expired token, a server-side call still running on the anonymous key, or a client that never attached the Authorization header. Confirm the session in your app layer before assuming the policy is wrong.

Why does my insert fail right after signup?

Because the profile row is written before the session exists, so the request runs as anon and auth.uid() is null. A policy written for authenticated never applies, and an ownership check against null never matches. The fix is a security definer trigger on auth.users that writes the user_profiles row server-side during signup, instead of inserting it from the client.

How do I fix this for Storage uploads?

Add an INSERT policy on storage.objects, scoped to the intended bucket and path. Storage keeps its policies on that table, so a policy on your application table does nothing for uploads. Add SELECT when the caller should read the object or when the observed upload fails while returning its metadata. An upsert also needs UPDATE.

Supabase’s Storage access-control guide covers the operation-specific permissions, and the upload troubleshooting page explains the separate RETURNING * failure. Both current instructions are reconciled in the Storage section above.

Does the service_role key bypass row-level security?

Yes, and that is exactly why it must never reach client-side code. The service_role key was designed to skip row-level security for server code you control, so putting it in a browser bundle or a mobile app hands every visitor the same bypass. Use it only in server functions, backend jobs, and other code a user cannot read.

How do I see which policies are on my table?

Run select policyname, cmd, roles, permissive, qual, with_check from pg_policies where tablename = 'your_table'; in the SQL editor. cmd is the operation covered, qual is the USING clause, and with_check is the WITH CHECK clause. No rows returned with RLS enabled means there is no policy to satisfy, and a row marked RESTRICTIVE can block the insert on its own no matter what your other policies say.

This error says an applicable policy rejected a proposed row. The full error details, calling role, table grants, INSERT policy, and returned-row behavior identify which control did it. After the fix, test the boundary with two accounts and an unauthenticated request. That is the same verification pattern behind the data-loss failures found in AI-built apps and the broader launch-readiness decision.