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.
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:
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:
{"statusCode":"403","error":"Unauthorized","message":"new row violates row-level security policy"}
Why it happens
- There's no
insertpolicy onstorage.objectsfor this bucket. A new bucket has no policies, so onlyservice_rolecan write to it. - The policy checks the path and the path doesn't match. A policy that expects
<user id>/file.pdfrejects2026/march.pdf. - The user isn't signed in, so the request runs as
anonand a policy writtento authenticateddoesn't apply. This often happens on the server when the client wasn't given the user's cookies. - You're using
upsert: true. Overwriting needsselectandupdatepermission as well asinsert. Without them, uploading a new file works but replacing one fails. - The policy names the wrong bucket:
bucket_idholds the bucket's id, which is usually its name, and it's case-sensitive.
How to fix it
Upload into a folder named after the user:
const { data: { user } } = await supabase.auth.getUser() const { error } = await supabase.storage .from('receipts') .upload(`${user.id}/2026-03.pdf`, file, { contentType: 'application/pdf' })Allow each user to insert into their own folder:
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 );If you upload with
upsert: true, addselectandupdatetoo: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 );Check the policies that exist:
select policyname, cmd, roles, qual, with_check from pg_policies where schemaname = 'storage' and tablename = 'objects';Uploads that should happen without a user (an import job, a webhook) belong on the server with the
service_rolekey, 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.