Skip to main content

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.

Updated 3 min readMarkdown
Supabase Storage upload blocked by row-level security: the policies you need

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: true needs 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

  1. Note the exact error, the bucket name, and the full path the app sends.
  2. In the Supabase dashboard, go to Storage > Policies and look at that bucket's policies, operation by operation.
  3. Look at the upload call: is upsert: true set?
  4. 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.

The 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.