TIC Association
Supabase Storage · error 42501

StorageApiError: new row violates row-level security policy.

Your upload was refused by a policy on the storage.objects table, not by the bucket, and making the bucket public does not change that. Four causes cover nearly every case, and the file path you pass to the upload call usually tells you which one you hit.

the error

What you are looking at.

StorageApiError: new row violates row-level security policy

What is this error? It is PostgreSQL refusing an insert into storage.objects: every file in Supabase Storage is a row in that table, and an upload is an insert into it. Row level security checks it exactly as it checks an insert into one of your own tables, which is why the message is the same one PostgreSQL raises for a table (that case is on the 42501 page). In Supabase's words: "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."

find your cause

Four causes. Check them in this order.

1

There is no insert policy on storage.objects for your role

The bucket exists, the file is fine, and nothing lets a signed-in user insert the row. Supabase: "the only RLS policy required for uploading objects is to grant the INSERT permission to the storage.objects table." This policy lets each signed-in user upload into a folder named after their own user id inside one bucket. Proven on a real PostgreSQL: with it, a user stores a file under their own id and is refused under anyone else's.

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

The path you upload to does not match the policy

The policy above compares the first folder of the object's name with the signed-in user's id. An upload to avatar.png with no folder, or to a folder named after a username, an email, or a different id, fails with the same message. Log the path you pass to the upload call: its first segment must be the user id of the session making the request, and the bucket in the call must be the bucket in the policy. You can read how the helper splits a path in your SQL editor:

select (storage.foldername('11111111-1111-1111-1111-111111111111/avatar.png'))[1];
3

You are overwriting a file (upsert)

Uploading with upsert: true onto a path that already exists turns the insert into an update of the existing row, and that row is checked against your update policies. Supabase: "To allow overwriting files using the upsert functionality you will need to additionally grant SELECT and UPDATE permissions." PostgreSQL says why the error is a refusal and not a silent skip: "unlike a standalone UPDATE command, if the existing row does not pass the USING expressions, an error will be thrown." Proven: the same overwrite is refused while only the insert policy exists and succeeds once these two are added, and the other user's folder stays closed.

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

create policy "users overwrite their own files"
on storage.objects for update to authenticated
using (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = (select auth.uid())::text
)
with check (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = (select auth.uid())::text
);
4

The request is not signed in

The policies say to authenticated, and a request with no session reaches the database as the anonymous role, which they do not cover. Proven: the anonymous role is refused by the same policy that lets the signed-in user through. In the browser, make sure a session exists right before the upload. On the server, the client has to be built from the request's cookies; Supabase: "Use the browser client in code that runs on the browser, and the server client in code that runs on the server." If auth.uid() is null on the request, no folder policy can match; that case has its own page.

what not to do

Two shortcuts that open the bucket to everyone.

Do not allow uploads with with check (true) for every role. It makes the error disappear because it stops checking anything. Proven on a real PostgreSQL: with the policy below in place, a request with no session at all stores an object in the bucket, so anyone holding your public key can fill it.

create policy "anyone can upload"
on storage.objects for insert to anon, authenticated
with check (true);

Do not upload with the service key from the browser. The service role bypasses every policy, which the suite confirms by storing an object with no policy that allows it. Supabase: "Service keys entirely bypass RLS policies, granting you unrestricted access to all Storage APIs. Remember you should not share the service key publicly."

questions

Straight answers.

Does making the bucket public fix the upload?
No. A public bucket changes who can read, not who can write. Supabase: "When a bucket is designated as 'Public,' it effectively bypasses access controls for both retrieving and serving files within the bucket." And in the same passage: "Access control is still enforced for other types of operations including uploading, deleting, moving, and copying." Uploads still need an insert policy on storage.objects.
Which policies does a profile photo upload need?
One insert policy on storage.objects for the authenticated role, scoped to the bucket and to a folder named after the user's id. If the app overwrites the photo with upsert, add a select policy and an update policy with the same scope. Nothing else.
The error names a table called objects, and I never created one.
It is Supabase's own table, storage.objects, where every uploaded file is a row. Your policies for uploads go on that table, not on the bucket and not on any table of yours.

One missing 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 42501 on one of your own tables, or 42P17, infinite recursion detected in policy?

Stuck on an upload that still fails after this? Paste the error into the free diagnosis at rescue.ticassociation.com and get the cause and the fix path back in about a minute.

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