Abbi Labs

Guides

Supabase "new row violates row-level security policy": fix it without opening your table

By Pritesh Ghodge, security engineer · Last updated 5 November 2026

Already "fixed" this error once? Check what the fix opened.

The Abbi Labs quick check scans your live app for leaked keys in its JavaScript, Supabase tables anyone can read with your public key, exposed .env/.git files and security headers. No sign-up. For write policies like with check (true), use the free RLS policy checker.

Scan your app free →

Weekly AI-app security checklist

One short email a week: a practical check for apps built with Supabase, Next.js and AI tools (RLS, leaked keys, headers and more). Unsubscribe anytime.

You click save in your Lovable, Bolt or Cursor app and get this back from Supabase:

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

That's Row Level Security doing its job. RLS is on for the table, and the row you tried to write didn't pass a policy. The error itself is safe. The danger is the fix: ask an AI assistant to "make the error go away" and the quickest answers are with check (true) or turning RLS off. Both make the error disappear, and both let users write rows they shouldn't.

This guide shows how to find which check failed and fix it so each user can only write their own rows.

USING vs WITH CHECK in 30 seconds

A Postgres policy has two expressions. From the Postgres CREATE POLICY docs and Supabase's RLS guide:

Step 1: see the policies on that table

In the Supabase SQL editor (replace todos with your table):

select policyname, cmd, roles, qual as using_expr, with_check
from pg_policies
where schemaname = 'public' and tablename = 'todos'
order by cmd;

Then match your situation below.

Cause 1: there's no INSERT policy

RLS is on, there's a SELECT policy, but nothing with cmd = INSERT or ALL. Default deny, so every insert fails. Add an owner-scoped one:

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

That lets a signed-in user add rows, but only rows whose user_id is their own id.

Cause 2: user_id doesn't match auth.uid() (often it's null)

The policy is right, but the row isn't. Usual reasons:

Cause 3: insert().select() or upsert() needs more than an INSERT policy

supabase-js .insert(row).select() asks Postgres to return the new row. Postgres then checks the new row against your SELECT policies too, and errors if it fails. Fix: add an owner-scoped SELECT policy, or drop .select() if you don't need the row back.

.upsert() becomes insert ... on conflict do update, which checks INSERT, SELECT and UPDATE policies. It needs all three, scoped to the owner:

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

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

Supabase's docs also note that an UPDATE needs a matching SELECT policy to work as expected.

Cause 4: an update tries to change the owner

If the error comes on UPDATE, the new version of the row failed WITH CHECK. Often the app sends the whole object back, including a user_id it shouldn't be changing, or a team/owner column that moved. That's the policy correctly stopping a row from being handed to someone else. Send only the fields that changed.

Cause 5: Supabase Storage uploads

Uploading a file inserts a row into storage.objects, so the same error means the bucket has no matching INSERT policy for that user. Upserting a file (upsert: true) also needs SELECT and UPDATE. Scope the policy to the bucket and the user's own folder:

create policy "users upload to own folder"
on storage.objects for insert to authenticated
with check (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = (select auth.uid())::text
);

The storage bucket guide covers private buckets, listing and signed URLs.

Reproduce it in SQL as the real user

The SQL editor runs as postgres, which bypasses RLS, so "it works in SQL" proves nothing. Run the insert as a signed-in user inside a transaction that rolls back:

begin;
set local role authenticated;
set local request.jwt.claim.sub = 'PASTE-A-REAL-USER-UUID';
insert into public.todos (title, user_id)
values ('test', 'PASTE-A-REAL-USER-UUID');
rollback;

If that passes, try it with a different user's id in user_id. It should fail. If it doesn't, your policy lets users create rows for each other.

The two fixes that make it worse

with check (true) is fine where anyone really may add any row, like a public waitlist or contact form. Even then, keep reads closed and consider a rate limit, because the public key lets anyone call it from a script.

A prompt for Lovable, Bolt or Cursor

I'm getting "new row violates row-level security policy" on <table>.
Don't disable RLS, don't use the service_role key in the frontend,
and don't use with check (true) or using (true).
1. Show me the current policies on this table (pg_policies).
2. Find which check fails: missing INSERT policy, user_id not set
   or not equal to auth.uid(), user not signed in, or .select()/upsert
   needing SELECT/UPDATE policies.
3. Fix it with owner-scoped policies TO authenticated using
   (select auth.uid()) = user_id, and set user_id default auth.uid()
   if the app doesn't send it.
Show me the SQL before applying it.

What an outside scan can and can't see

From outside, a scanner can see your live JavaScript and whether a table is readable with your public key. It can't see your policies and shouldn't test writes against a live app. That's why write problems like with check (true) need the policy checker or the SQL test above, and a two-account test: sign in as user B and try to create or change a row that belongs to user A.

Keep it that way

Policies get loosened in the middle of a late-night "fix this error" session and nobody notices until the data is somewhere it shouldn't be. Abbi Labs scans your live app for leaked keys, Supabase tables readable with your public key, exposed .env/.git files and security headers. Monitor re-scans weekly and after every deploy, and emails you only when something new appears.

Run a free scan

FAQ

What does "new row violates row-level security policy" mean in Supabase?

RLS is on for the table and the row you tried to write didn't pass any permissive policy's WITH CHECK expression (or, for UPDATE and FOR ALL policies without WITH CHECK, the USING expression). The most common causes are: no INSERT policy at all, a user_id that doesn't match auth.uid() (often null because the column wasn't sent or the user isn't signed in), and insert().select() or upsert() needing a SELECT or UPDATE policy as well.

Is it OK to fix it with WITH CHECK (true)?

Only for tables where anyone really should be able to add any row, like a public contact form, and even then scope it TO the role that needs it and limit what columns they can set. On a table with user-owned rows, with check (true) lets any signed-in user (or anyone, if the policy targets anon or public) insert rows with someone else's user_id. Use with check ((select auth.uid()) = user_id) instead.

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

The SQL editor runs as the postgres role, which bypasses RLS. Your app runs as anon or authenticated, so the policies apply. To reproduce the app's view in SQL, use set local role authenticated and set local request.jwt.claim.sub inside a transaction, as shown on this page.

Why does insert work but insert().select() fail?

Returning the new row needs read access too. Postgres checks the new row against the table's SELECT policies when the query has a RETURNING clause, which is what .select() adds. Add an owner-scoped SELECT policy, or drop .select() if you don't need the row back.

I get this error uploading to Supabase Storage. Is it the same thing?

Yes. Uploads insert a row into storage.objects, so the bucket needs an INSERT policy on storage.objects for the uploading role, and upsert: true also needs SELECT and UPDATE policies. Scope them to the bucket and the user's own folder rather than allowing any upload.

Does Abbi Labs check this?

The free RLS policy checker flags INSERT and UPDATE policies with WITH CHECK (true), UPDATE policies with USING (true), tables with RLS off, and policies on user-owned tables that never reference auth.uid(), from one read-only SQL query, analysed in your browser. The no-sign-up quick check scans your live site from outside: leaked keys in your JavaScript, Supabase tables readable with your public key, exposed .env/.git files and security headers. It doesn't test writes.

Related guides

Weekly AI-app security checklist

One short email a week: a practical check for apps built with Supabase, Next.js and AI tools (RLS, leaked keys, headers and more). Unsubscribe anytime.