# Storage 403: new row violates row-level security policy on upload

> Why a Storage upload fails with 403 "new row violates row-level security policy", and the storage.objects policies you need for uploads, upserts and per-user folders.

Source: https://supahost.app/kb/storage-rls-upload-403 · Updated 2026-11-11

Every Storage upload inserts a row into `storage.objects`, and that table has Row Level Security. If no policy allows the insert for this user, this bucket and this path, Storage refuses the upload with 403.

## What you see

From supabase-js:

```js
const { error } = await supabase.storage.from('receipts').upload('2026/march.pdf', file)
console.log(error.message) // "new row violates row-level security policy"
```

The HTTP response is a 403 with:

```json
{"statusCode":"403","error":"Unauthorized","message":"new row violates row-level security policy"}
```

## Why it happens

1. **There's no `insert` policy on `storage.objects`** for this bucket. A new bucket has no policies, so only `service_role` can write to it.
2. **The policy checks the path and the path doesn't match.** A policy that expects `<user id>/file.pdf` rejects `2026/march.pdf`.
3. **The user isn't signed in**, so the request runs as `anon` and a policy written `to authenticated` doesn't apply. This often happens on the server when the client wasn't given the user's cookies.
4. **You're using `upsert: true`.** Overwriting needs `select` and `update` permission as well as `insert`. Without them, uploading a new file works but replacing one fails.
5. **The policy names the wrong bucket**: `bucket_id` holds the bucket's id, which is usually its name, and it's case-sensitive.

## How to fix it

1. Upload into a folder named after the user:

   ```js
   const { data: { user } } = await supabase.auth.getUser()
   const { error } = await supabase.storage
     .from('receipts')
     .upload(`${user.id}/2026-03.pdf`, file, { contentType: 'application/pdf' })
   ```

2. Allow each user to insert into their own folder:

   ```sql
   create policy "Users upload own receipts"
   on storage.objects for insert to authenticated
   with check (
     bucket_id = 'receipts'
     and (storage.foldername(name))[1] = (select auth.uid())::text
   );
   ```

3. If you upload with `upsert: true`, add `select` and `update` too:

   ```sql
   create policy "Users read own receipts"
   on storage.objects for select to authenticated
   using (
     bucket_id = 'receipts'
     and (storage.foldername(name))[1] = (select auth.uid())::text
   );

   create policy "Users replace own receipts"
   on storage.objects for update to authenticated
   using (
     bucket_id = 'receipts'
     and (storage.foldername(name))[1] = (select auth.uid())::text
   );
   ```

4. Check the policies that exist:

   ```sql
   select policyname, cmd, roles, qual, with_check
   from pg_policies
   where schemaname = 'storage' and tablename = 'objects';
   ```

5. Uploads that should happen without a user (an import job, a webhook) belong on the server with the `service_role` key, which bypasses these policies. Never put that key in the browser.

## On SupaHost

Storage works as it does in open-source Supabase, so the same policies apply. Create them in Studio's SQL editor or the Storage policies screen (open Studio with the **Studio** button in the project header; owner only), or with a migration through MCP.

A file above 50 MB fails with 413, not 403. See [upload file size limits](/kb/upload-size-limit).

## Related

- [Storage access control](/docs/storage/access-control)
- [Why can't I upload to or list my public bucket?](/kb/public-bucket-upload)
- [Folder operations and hierarchical policies](/kb/storage-folder-operations)
- [Uploading files](/docs/storage/uploads)
