# 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:

```sql
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:

```sql
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](https://supabase.com/docs/guides/storage/security/access-control).

---

This fix first appeared on our site: [Supabase Storage upload "new row violates RLS": fix with SQL](https://stacksmesh.com/fix/supabase-storage-upload-row-level-security?utm_source=hashnode&utm_medium=article&utm_campaign=BR-0003). 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](https://stacksmesh.com/tools/launch-check?utm_source=hashnode&utm_medium=article&utm_campaign=BR-0003).

*I wrote this with AI assistance and reviewed it myself.*

