Back to Blog
Engineering 7 min read

Fix: "new row violates row-level security policy" in Supabase

The Supabase "new row violates row-level security policy" error means RLS blocked your write. Why it happens on insert, select, upsert and storage uploads, and the exact SQL to fix it.

By the Amex Technology Team

Fix: "new row violates row-level security policy" in Supabase — guide by Amex Technology

No time to debug this Supabase issue? Tell us what's broken and our developers will get it working, usually within 24 hours.

Contact Us

What This Error Means

The Supabase error new row violates row-level security policy for table "posts" (Postgres error code 42501) means Row Level Security blocked your write. RLS is enabled on the table, and no policy's WITH CHECK condition passed for the row you tried to insert or update.

The fix is almost always one of three things: add an INSERT policy, make sure the row's user_id matches auth.uid(), or make sure the request is actually authenticated. If you also read the row back with .select() or use upsert, you need SELECT and UPDATE policies too.

PostgreSQL insert blocked by a Supabase row-level security STOP check

Quick Diagnosis

Find the line that matches what you're doing:

You're doingPolicies you need
insert()INSERT
insert().select()INSERT and SELECT
update()UPDATE (and SELECT to find the row)
upsert()INSERT, UPDATE and SELECT
Storage upload()INSERT on storage.objects
Storage upload() with upsert: trueINSERT, SELECT and UPDATE on storage.objects

If the matching policies exist, the problem is the values in the row or a missing session, covered below.

The Most Common Causes

  1. RLS is on, but there's no INSERT policy. RLS denies everything by default, so enabling it without policies blocks every write.
  2. The WITH CHECK condition fails. A policy exists, but the row doesn't satisfy it, usually because user_id doesn't match auth.uid().
  3. auth.uid() is null. The request isn't authenticated, so any policy comparing to auth.uid() fails.
  4. You return the row without a SELECT policy. insert().select() has to read the new row back. Without a SELECT policy, Postgres reports the same error even though your INSERT policy is fine.
  5. You use upsert with only an INSERT policy. Upsert can turn into an UPDATE, so it needs UPDATE (and SELECT) policies as well.

How to Fix It

1. Add an INSERT policy

Most tables holding per-user data need a policy like this:

alter table posts enable row level security;
create policy "Users can insert their own posts"
on posts for insert
to authenticated
with check ((select auth.uid()) = user_id);

The with check clause is what INSERT and UPDATE validate against. If it evaluates to false, you get the error. Wrapping auth.uid() in (select ...) is Supabase's recommended form: Postgres evaluates it once per query instead of once per row, which matters on large tables.

Supabase SQL Editor with an INSERT policy checking auth.uid() against user_id, RLS policy active on the posts table

2. Add SELECT and UPDATE policies if you read back or upsert

create policy "Users can read their own posts"
on posts for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Users can update their own posts"
on posts for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

The UPDATE policy uses both clauses: using decides which existing rows the user can change, and with check stops them from changing a row into one they don't own.

3. Set user_id correctly, or let the database do it

Because the policy checks auth.uid() = user_id, the row must contain the current user's id:

const { data: { user } } = await supabase.auth.getUser();
await supabase.from('posts').insert({
  title: 'Hello',
  user_id: user.id, // must match auth.uid()
});

A safer option is to set it in the database, so the client can't get it wrong:

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

Then insert without user_id at all, and the default fills it in from the logged-in user's token.

Client insert call with user_id set to user.id so it matches the RLS policy's auth.uid() check

4. Confirm the request is authenticated

If auth.uid() is null, the request reached Supabase without a valid session and ran as the anon role. Common reasons:

Note that your policy says to authenticated. Requests from logged-out visitors run as anon and will never match it, which is exactly what you want for private data.

RLS policy check flow: an incoming insert request either fails as unauthenticated or passes when auth.uid() matches user_id

5. Fix storage uploads

The same error appears when uploading files to Supabase Storage, because files are rows in the storage.objects table. A common setup lets each user upload into a folder named after their user id:

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.uid())::text
);

Upload to a path like ${user.id}/avatar.png. If you pass upsert: true to replace an existing file, also add SELECT and UPDATE policies on storage.objects with the same conditions.

6. Test as a real user, not in the SQL editor

The Supabase SQL editor runs as the postgres role, which bypasses RLS. Your insert "works" there while real users still fail. You can simulate a logged-in user inside a transaction instead:

begin;
set local role authenticated;
set local request.jwt.claims = '{"sub": "PASTE-A-USER-UUID", "role": "authenticated"}';
insert into posts (title, user_id)
values ('test', 'PASTE-A-USER-UUID');
rollback;

If this fails with the same error, the policy is the problem. If it passes, the problem is in your app: the session or the values you send.

What About the Service Role Key?

The service_role (or new sb_secret_...) key bypasses RLS completely, so the error disappears if you use it. That's only acceptable in trusted server code such as cron jobs, webhooks and admin tasks. Never put it in the browser or in a NEXT_PUBLIC_ variable. Using it to silence this error in client code gives every visitor full access to your database.

How to Prevent It

  • Add policies in the same migration where you enable RLS.
  • Default user_id to auth.uid() at the database level.
  • Write separate, explicit policies for SELECT, INSERT, UPDATE and DELETE.
  • Check the Advisors → Security page in the Supabase dashboard for tables with RLS disabled or missing policies.
  • Test as a real user before every release.

Apps generated with Lovable, Bolt or Replit often ship with RLS turned off or with policies that are too loose. Our free App Checker looks for exposed keys and other common problems.

Frequently Asked Questions

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

The SQL editor runs as the postgres role and bypasses RLS. Your app uses the anon or authenticated role, which RLS enforces, so a missing or failing policy blocks the write.

Do I need a SELECT policy as well as an INSERT policy?

If you return the inserted row with .select(), yes. Postgres has to read the new row back, and without a SELECT policy it raises the same error even when the INSERT policy passes.

Why does upsert fail when insert works?

An upsert may update an existing row, so Postgres checks INSERT, UPDATE and SELECT policies. Add UPDATE and SELECT policies for the same rows.

What is the difference between USING and WITH CHECK?

USING filters which existing rows a query can see or change. WITH CHECK validates the values of new or updated rows. INSERT uses WITH CHECK; UPDATE uses both.

Why do I get this error when uploading to Supabase Storage?

Storage files are rows in the storage.objects table, which has RLS enabled. Add an INSERT policy on storage.objects for your bucket, and SELECT and UPDATE policies if you upload with upsert.

Can I disable RLS to fix this quickly?

Only for local debugging. In production, a table with RLS disabled can be read and changed by anyone with your public API key.

Need Supabase Set Up Properly?

RLS errors are usually a sign the data layer needs a careful review. At Amex Technology, we design Supabase schemas, policies and auth flows for web apps and SaaS products, and we fix broken ones within 24 hours through our bug fix service. See our portfolio or get in touch.

Fixed it? Check the rest of your Supabase app for free. Our App Checker scans for exposed API keys, public Supabase service keys, SEO problems and slow loading in about 30 seconds.

Check my app free
Supabase Row-Level Security Postgres Troubleshooting

Bug Fix Service

Still stuck? We'll fix your Supabase error for you.

Skip the hours of trial and error. Send us the error and access to your project, and a senior developer will get it working, usually within 24 hours. We'll review it for free before any work starts.

  • Your bug or error fixed by a senior developer
  • Most fixes delivered within 24 hours
  • Short write-up of the root cause and what changed
  • Free assessment before any work starts
Typically responds within 4 hours

Ready to build your next digital product?

Tell us what you're building. We'll respond with a clear plan, honest scope estimate, and a timeline — no obligations.

No commitment required
Free consultation
4-hour response
No-commitmentfirst call
4hresponse time
5+ yearsexperience