Supabase Storage upload blocked by row-level security: the policies you need
Why new buckets reject every upload, how to find the missing policy, and the SQL for per-user folders.

You add a file upload to your app, test it, and Supabase answers with "new row violates row-level security policy". Sometimes it's "StorageApiError" in front, sometimes just "Unauthorized".
Nothing is broken. Storage is doing what it was built to do: refuse uploads until a policy on the storage.objects table allows them. The Supabase docs put it this way: "By default Storage does not allow any uploads to buckets without RLS policies."
Four reasons the upload is refused
- The bucket has no policies yet. New buckets start closed.
- You're using upsert. Overwriting a file with
upsert: trueneeds SELECT and UPDATE policies on top of INSERT. - The path and the policy disagree. The policy expects a folder named after the user; the app uploads to the bucket root or somewhere else.
- Nobody is signed in. A policy that checks the user's id can't pass for a request without a session.
Pin down which one it is
- Note the exact error, the bucket name, and the full path the app sends.
- In the Supabase dashboard, go to Storage > Policies and look at that bucket's policies, operation by operation.
- Look at the upload call: is
upsert: trueset? - Log whether there's a session at the moment of upload.
The fix: one folder per user
Have the app upload to a path that starts with the user's id, like avatars/<user-id>/photo.png. Then add an INSERT policy that compares the first folder in the path with the signed-in user, and a SELECT policy so people can read what they uploaded:
create policy "Users 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')
);
create policy "Users read their own files"
on storage.objects for select
to authenticated
using ( bucket_id = 'avatars' and (select auth.jwt()->>'sub') = owner_id );
storage.foldername(name) splits the object path into folders, and auth.jwt()->>'sub' is the signed-in user's id. That pairing is the one Supabase's own examples use.
Only if you overwrite files
When the app replaces files in place, add UPDATE with the same folder check:
create policy "Users overwrite files in their own folder"
on storage.objects for update
to authenticated
using (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
)
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);
If you don't need overwrites, give each upload a new file name and keep upsert off.
Test it as three people
- User A, own folder: the upload succeeds.
- User A, user B's folder: the upload fails.
- Signed out: the upload fails.
The two failures are the real test. A policy that allows everything passes the first check too.
Keep it fixed
- Decide public or private for each bucket before building the upload screen. Public is for files anyone may see, like product images; invoices and personal documents go in a private bucket.
- Never upload from the browser with a secret or service key. It skips every policy.
Source: Supabase: Storage access control.
This fix first appeared on our site: Supabase Storage upload "new row violates RLS": fix with SQL. If you want a one-minute check of your live app for exposed keys, missing security headers and broken basics, try the free Launch Check.
I wrote this with AI assistance and reviewed it myself.
About the author
- Founder and CEO of StacksMesh (stacksmesh.com). We fix, launch and migrate apps built with Base44, Lovable, Bolt and Replit, with a fixed price in writing before any work. I write about making AI-built apps safe and production-ready.
Comments
Join the discussionThe bucket-level check is easy to miss when the error sounds like a table policy. Testing one upload as the signed-in user, not as a service key, usually shows which policy is actually blocking it.

