Supabase “new row violates row-level security policy”: why and the safe fix
Short answer: If your Lovable, bolt.new or Cursor app gets new row violates row-level security policy (code 42501) from Supabase, Row Level Security (RLS) blocked a write. Either no insert or update policy admits that row, the row belongs to someone else, or the request isn't signed in. Fix the policy or the row. Never fix it by turning RLS off or putting the secret key in your app.
Free: the 10-check security checklist from our scanner test. Download the PDF, no signup. Or get an email when it changes.
What the error means
Supabase protects each table with RLS policies. A policy is a rule that says who may see or change which rows. For writes, the policy's with check rule decides which new rows are allowed. Supabase's RLS docs say the example is there "so a user cannot create a row that belongs to someone else". When the check comes back false or null, PostgreSQL cancels the whole command. That's in the CREATE POLICY docs. That cancelled write is this error.
PostgREST is the layer that turns your app's requests into database queries. Per its error codes, your app sees this error as HTTP 403 when the user is signed in and 401 when they aren't. This is the body we got in our test:
HTTP 403
{
"code": "42501",
"details": null,
"hint": null,
"message": "new row violates row-level security policy for table \"notes\""
} This is RLS doing its job. The table isn't broken. The write didn't match a rule. It's also different from "permission denied for table". That error has the same code but means the role has no grant at all. A role is who Supabase thinks is asking, and a grant lets a role use a table. That one is covered in our 42501 fix guide. Since Oct 30, 2026, tables you create on Supabase no longer get those grants automatically, per the Supabase changelog. So a brand-new table usually hits "permission denied" before it can ever reach this error.
Find your cause in 30 seconds
- Table has no insert or update policy? Cause 1.
- Not sending
user_id, or sending one that isn't the signed-in user's? Cause 2. - Fails right after sign-up, or the status is 401? Cause 3.
- Using
.select()after the insert, or.upsert()? Cause 4. - It's a file upload? Cause 5.
To see what the table has right now, run this in the SQL Editor. Change notes to your table.
-- Which policies exist on the table, for which operation and role
select policyname, cmd, roles, qual as using_expr, with_check
from pg_policies
where schemaname = 'public' and tablename = 'notes';
-- Does the role even have the grant? (false = "permission denied", not this error)
select has_table_privilege('authenticated', 'public.notes', 'INSERT') as can_insert,
has_table_privilege('authenticated', 'public.notes', 'SELECT') as can_select; This is a BuildWithThis query built on the Postgres catalog, not a Supabase snippet. We didn't run this exact query in the Oct 25 test.
Cause 1: no policy for that operation
Policies are per operation. A select policy lets users read. It doesn't let them insert. With only a select policy, Alice inserting her own note got 403 "new row violates row-level security policy". Supabase's RLS docs recommend a separate policy for each of select, insert, update and delete.
-- Let a signed-in user insert only rows they own
create policy "owner can insert" on public.notes
for insert to authenticated
with check ((select auth.uid()) = user_id); From Supabase's Row Level Security docs. Tested by BuildWithThis on Oct 25, 2026. Alice's insert went from 403 to 201.
Cause 2: the row belongs to someone else, or to nobody
The policy above only admits rows whose user_id is the signed-in user. If the app sends another user's id, or leaves user_id out so it's null, the check fails. In our test both returned 403. Send the user's id, or let the database fill it in:
-- Fill user_id from the session when the app leaves it out
alter table public.notes alter column user_id set default auth.uid(); This is a BuildWithThis pattern, not one from Supabase's docs. Tested Oct 25, 2026. An insert without user_id went from 403 to 201, saved with Alice's id. Keep the insert policy. When Bob sent Alice's id on purpose, the default didn't apply and the policy still returned 403.
// Either send the signed-in user's id...
const { data: { session } } = await supabase.auth.getSession()
await supabase.from('notes').insert({ user_id: session.user.id, body: 'hello' })
// ...or, with the column default above, leave it out
await supabase.from('notes').insert({ body: 'hello' }) BuildWithThis example, supabase-js 2.109.0. Tested Oct 25, 2026. Both forms returned 201.
Updates hit the same wall. An update policy has a using rule, which says which rows you may change. It also has a with check rule, which says what they may look like afterwards. When Alice tried to change her note's user_id to Bob's, she got 403 with the same message.
create policy "owner can update" on public.notes
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id); BuildWithThis policy in the shape of Supabase's update example in the RLS docs. Tested Oct 25, 2026.
Cause 3: the request isn't signed in
With no session, or an expired one, auth.uid() is null, so every "owner" check fails, per the RLS docs. Logged-out requests get 401, not 403. anon is the logged-out role. If it has no insert grant, the message is "permission denied for table". If it has one, the message is "new row violates row-level security policy". We got both.
Watch for the sign-up trap. With email confirmation on, supabase.auth.signUp() in our test returned the new user with session: null. An insert straight after went out logged out and failed with 401. Signing in before confirming returned email_not_confirmed. After the email was confirmed, sign-in gave a session and the same insert returned 201. Fix it in the app: wait for a session before writing.
const { data: { session } } = await supabase.auth.getSession()
console.log(session ? session.user.id : 'not signed in: auth.uid() will be null') BuildWithThis check. Tested Oct 25, 2026. getSession() returned no session right after sign-up.
You may need a row, such as a profile, the moment someone signs up. Supabase's Managing user data docs show a database trigger for that. They warn that if the trigger fails, it can block sign-ups. We didn't test it. Our login tool test covers picking the auth setup.
Cause 4: .select() or .upsert() also needs a select policy
The insert itself can be allowed and the request still fail with this message. Getting the row back uses RETURNING, and an upsert uses ON CONFLICT. PostgreSQL checks both against the table's select policies, per CREATE POLICY. supabase-js only asks for rows back when you chain .select(). See the insert and upsert reference.
create policy "owner can read" on public.notes
for select to authenticated
using ((select auth.uid()) = user_id); From Supabase's Row Level Security docs. Tested by BuildWithThis on Oct 25, 2026.
// No row back: needs the insert policy only
await supabase.from('notes').insert({ body: 'hello' })
// Row back, or upsert: also needs the select policy above
await supabase.from('notes').insert({ body: 'hello' }).select()
await supabase.from('notes').upsert({ id, body: 'hello' }, { onConflict: 'id' }) BuildWithThis example, supabase-js 2.109.0. Results below.
| Call (signed in, own row) | Prefer header sent | Result, insert policy only |
|---|---|---|
.insert(row) | none | 201 |
.insert(row).select() | return=representation | 403, 42501 |
.upsert(row, { onConflict: 'id' }), new row | resolution=merge-duplicates | 403, 42501 |
.upsert(…, { ignoreDuplicates: true }), new row | resolution=ignore-duplicates | 403, 42501 |
| Same three after adding the select policy | 201 | |
.upsert() onto an existing own row, no update policy | resolution=merge-duplicates | 403, “new row violates row-level security policy (USING expression)…”. Row unchanged |
| Same, after adding the update policy | 200, row updated |
Two of these surprised us. A plain .upsert() of a brand-new row failed without any .select(). And an upsert that overwrites an existing row also needs an update policy. Without one, the message gains "(USING expression)". If you don't need the row back, dropping .select() also works. Don't reach for { returning: 'minimal' }, because it isn't an option in the current insert reference.
Cause 5: file uploads (Storage)
Storage keeps files as rows in storage.objects, with the same RLS. It allows no uploads until you add policies. A plain upload needs only an insert policy, per Storage access control. Supabase's usual pattern puts each user's files in a folder named after their user id. Upload anywhere else and you get this error, even with upsert: false.
-- Upload into your own folder: user-files/<your user id>/...
create policy "users upload to own folder" on storage.objects
for insert to authenticated
with check (
bucket_id = 'user-files'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);
-- Only if you upload with upsert: true. Select lets upsert create new files...
create policy "users read own folder" on storage.objects
for select to authenticated
using (
bucket_id = 'user-files'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);
-- ...and update lets it overwrite an existing file
create policy "users update own folder" on storage.objects
for update to authenticated
using (
bucket_id = 'user-files'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
)
with check (
bucket_id = 'user-files'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
); From Supabase's Storage access control docs, adapted to a private bucket called user-files. Tested by BuildWithThis on Oct 25, 2026. Add the select and update policies only if you upload with upsert: true.
const { data: { session } } = await supabase.auth.getSession()
const path = session.user.id + '/avatar.png' // first folder = your user id
const { error } = await supabase.storage
.from('user-files')
.upload(path, file, { upsert: true })
// error.statusCode is '403' for this error, '409' for "already exists" BuildWithThis example, supabase-js 2.109.0. Tested Oct 25, 2026.
Supabase's two Storage pages disagree, so we tested it. The troubleshooting page says this error "only happens" with upsert: true and is fixed by a select policy. The access control page says overwriting needs select and update. In our test the access control page was right about overwriting. Select alone let upsert create new files, but replacing an existing file still failed until we added the update policy. The troubleshooting page's "only with upsert" is also too narrow. Bob uploading into Alice's folder with upsert: false got the same error.
| Upload as Alice to her folder | Insert policy only | + select policy | + update policy |
|---|---|---|---|
New file, upsert: false | 200 | 200 | 200 |
Same path again, upsert: false | 400, body "statusCode":"409" “The resource already exists” | same | same |
New file, upsert: true | 400, body "statusCode":"403" “new row violates row-level security policy” | 200 | 200 |
Overwrite existing file, upsert: true | 400, body “403” (as above) | 400, body “403”. File unchanged | 200, file replaced |
| Bob uploads into Alice’s folder | 400, body “403” | 400, body “403” | 400, body “403” |
Note the status. Storage answered HTTP 400 and put the real code in the body. It was "statusCode":"403", or "409" for a duplicate. supabase-js gives you that as error.statusCode. The duplicate message we got was "The resource already exists", not the "Asset already exists" the troubleshooting page quotes. Picking a file-upload service? See our upload tool test.
The fixes that make it worse
Each of these makes the error go away by opening your data.
- Turning RLS off. Anyone with your project URL and the public key can then read, change and delete the table. Our RLS Disabled in Public guide shows a logged-out request doing all four.
with check (true). It stops the error and lets any signed-in user create rows for anyone. Don't use it:
In our test it let Bob create a note owned by Alice, which then appeared in Alice's list.create policy "anyone can insert" on public.notes for insert to authenticated with check (true);- The secret or
service_rolekey in the app. It bypasses RLS completely, and anyone can copy it out of a browser or app, per the RLS docs. Keep it on a server. The publishable key is safe to ship only with RLS on, per Securing your data. See publishable vs anon key. - A
security definerfunction inpublicto "get around" RLS. Supabase's RLS docs warn it can be called over the API with its creator's rights. Never put one in an exposed schema, which is a folder of tables your API can reach. to anon … using (true)on data that isn't public. It gives every logged-out visitor every row, per the RLS docs.
Prove it’s fixed
Use two test accounts, A and B, on a project you own. Signed in as B, insert a row with A's user_id, change one of your rows to A's id, and upload into A's folder. Each should fail with this error. Then, signed in as A, check that your own inserts, uploads and overwrites work.
Request to notes | What we got |
|---|---|
| Select policy only, Alice inserts her own note | 403, 42501, “new row violates row-level security policy for table "notes"” |
| Owner insert policy added, Alice inserts her own note | 201 |
Bob inserts a note with Alice’s user_id | 403, 42501, same message |
Alice inserts without user_id (no default) | 403, 42501, same message |
Same, after default auth.uid() | 201, row saved with Alice’s id |
Alice changes her note’s user_id to Bob’s | 403, 42501, same message |
Logged out, anon has no insert grant | 401, 42501, “permission denied for table notes” |
Logged out, anon has an insert grant | 401, 42501, “new row violates row-level security policy for table "notes"” |
Then check the data as well as the status code. A blocked update or delete often doesn't error at all. Bob's update of Alice's note returned 204 and changed nothing. Inserts and upserts are different. They fail loudly with this error. For a test that runs on every change, our RLS Disabled in Public guide has a pgTAP file. We didn't re-run it for this page.
What scanners don’t catch
This error is RLS refusing a write, so no scanner flags it. The dangerous part is the fix. A policy like with check (true) passes Supabase's Security Advisor, because RLS is still on. In our test of 6 free scanners, Advisor didn't flag a policy that let any signed-in user read every profile, and only a second-user test caught it. Advisor's RLS check also doesn't flag some reachable tables. Run the free vibe-coding security checklist for the checks scanners don't flag.
What we tested
We ran this test on Oct 25, 2026, at about 4 AM ET, on a Supabase lab project. It was on the Free plan with Postgres 17.11 in ca-central-1. We made a notes table and a private user-files bucket with SQL and two confirmed test users, alice and bob at example.com. We sent requests through supabase-js 2.109.0 and the REST and Storage APIs, logged out and signed in, adding one policy at a time. For the sign-up test we created a third user through the real sign-up API with email confirmation on. Supabase rejected the example.com address, so we used a test inbox. We confirmed that user with SQL instead of clicking the email link. Then we deleted every test table, file, bucket, policy and user.
One check is excluded. Our first re-run of the restored insert policy ran at the same moment as the change and got 201. The policy list was correct straight after, and the re-run got 403. Both are in the test log.
We didn't test the Supabase CLI's local stack, the dashboard's policy templates, the pgTAP file, the sign-up trigger, the catalog query above, and code generated by AI app builders. We make no claims about Lovable, bolt.new or Cursor here.
Tested on: Oct 25, 2026 · Supabase Free plan · Postgres 17.11 · supabase-js 2.109.0. SQL from Supabase docs unless labelled BuildWithThis. Re-test due Nov 30, 2026. This page was written and fact-checked by AI agents, who also ran the test. Every result is in our test log.
Questions people ask
What does “new row violates row-level security policy” mean?
Row Level Security refused a write. Postgres checks every new or changed row against the table’s with check rules, and if none lets the row in, the whole insert, update or upsert is cancelled. In our test it appeared for a missing insert policy, a row with someone else’s or no user_id, a missing select policy behind .select() or upsert, and Storage uploads.
Why does it say 403 (or 401)?
PostgREST, Supabase’s Data API, turns Postgres code 42501 into HTTP 403 when the request is signed in and 401 when it isn’t. We got exactly that. Storage is different: in our test it answered HTTP 400 and put "statusCode":"403" in the body.
Is this the same as “permission denied for table”?
No, though both are code 42501. “Permission denied” means the role has no grant on the table, and it fails before any policy runs. “New row violates” means the grant is there and a policy rejected the row. Our 42501 fix guide covers the grant problem.
I have an insert policy. Why does it still fail?
Check three things. Does the row’s user_id match the signed-in user (and is it set at all)? Is the request actually signed in? Are you calling .select() or .upsert(), which also need a select policy? Each one gave us the same error with an insert policy in place.
Why does .insert().select() fail when .insert() works?
Asking for the row back makes Postgres use RETURNING, and PostgreSQL says returned rows must pass the table’s select policies. supabase-js only asks for rows back when you chain .select(). In our test .insert() returned 201 and .insert().select() returned 403 until we added a select policy.
Why does upsert fail but insert works?
An upsert is INSERT … ON CONFLICT, and PostgreSQL checks proposed rows against select policies for it. In our test a plain .upsert() of a brand-new row failed without .select() and without a select policy. Overwriting an existing row also needed an update policy.
Why do my Storage uploads fail with this error?
Uploads need an insert policy on storage.objects, and the file path has to match it (for example, the first folder must be your user id). With upsert: true, Supabase’s two Storage pages disagree. In our test a select policy let upsert create new files, but overwriting an existing file also needed an update policy, as the Access Control page says.
Why does it fail right after sign-up?
With email confirmation on, our sign-up returned a user but no session, so the next insert went out logged out. It failed with 401, and signing in before confirming returned email_not_confirmed. After confirming, the same insert worked. If you need a row at sign-up, use Supabase’s trigger pattern.
Why is auth.uid() null in my policy?
Supabase says auth.uid() is null when the request carries no user access token: nobody signed in, or the session expired. A policy like auth.uid() = user_id then never passes. Log the session in the app before the insert.
Can I just use with check (true) or turn RLS off?
No. In our test with check (true) let Bob create notes owned by Alice, and they showed up in Alice’s list. Turning RLS off is worse: anyone with your project URL can read and change the table. Fix the policy or the row instead.
Should I use the service_role or secret key so it works?
Only on a server. Supabase says the secret and service_role keys bypass RLS, so in a browser or app anyone could copy one and read or change everything. Keep the publishable key in the app and fix the policy.
How do I prove my policies are right?
Use two test accounts. As user B, try to insert a row owned by A, change your row’s user_id to A’s, and overwrite A’s file. Each should fail. Then check A’s data is unchanged, because a blocked update can still return success: Bob’s update of Alice’s note returned 204 and changed nothing.
Sources
- Supabase docs: Row Level Security (checked 2026-10-24)
- PostgreSQL 17 docs: CREATE POLICY (checked 2026-10-24)
- PostgREST docs: Errors (HTTP status per Postgres code) (checked 2026-10-24)
- Supabase JavaScript reference: insert (checked 2026-10-24, re-checked 2026-10-30)
- Supabase JavaScript reference: upsert (checked 2026-10-24)
- Supabase docs: Storage access control (checked 2026-10-25, re-checked 2026-10-30)
- Supabase troubleshooting: Storage 403 "new row violates row-level security policy" on upload (checked 2026-10-25, re-checked 2026-10-30)
- Supabase docs: Managing user data (checked 2026-10-24)
- Supabase docs: Securing your data (checked 2026-10-24)
- Supabase changelog: tables not exposed to the Data and GraphQL API automatically (checked 2026-10-30)
- Our test on 2026-10-25 (Supabase Free plan, supabase-js 2.109.0, REST and Storage APIs, three test users)
Related: Supabase 42501 fix guide · RLS Disabled in Public · publishable vs anon key · what Security Advisor doesn't flag · Vibe coding hub
General guidance from one small test project, not a security audit. Check only projects you own.
Get the vibe coding security checklist
Ten checks to run before you launch, from our test of six scanners. We re-test it every 30 days. Subscribe and we'll email you when it changes. Want it now? Download the PDF, no signup needed.
We re-check this fix by November 30 and email you if Supabase’s behaviour changes. Free weekly email.
We'll email you a link to confirm. We use Kit to send the email. Unsubscribe anytime. Privacy