Symptoms
- Uploading an avatar, image or document fails with HTTP 403 and
new row violates row-level security policy. - The upload works in the dashboard but not from your app.
- First uploads work, but replacing a file (
upsert: true) fails. - Deleting a file does nothing, with no error, or fails.
Why it happens
From Supabase's docs: "By default Storage does not allow any uploads to buckets without RLS policies. You selectively allow certain operations by creating RLS policies on the storage.objects table" (Supabase: Storage access control).
The causes, in the order we'd check them:
- No
INSERTpolicy for the bucket, or one that doesn't match the path you upload to. - No matching
SELECTpolicy. Supabase's troubleshooting article explains that the Storage API runs anINSERT ... RETURNING *. If noSELECTpolicy covers the new object, "the database is unable to return the row metadata" and the whole upload fails, even if yourINSERTpolicy is correct (Supabase troubleshooting: Storage 403 on upload). upsert: truewithoutSELECTandUPDATE. "To allow overwriting files using the upsert functionality you will need to additionally grant SELECT and UPDATE permissions" (Storage access control).- The path doesn't match the policy. A policy that only allows
<user id>/...rejects an upload toavatars/photo.png. - The user isn't signed in. A policy
to authenticatedwon't match a request made with only the public key and no user session.
Step-by-step fix
Run these in the Supabase SQL Editor. Replace avatars with your bucket id.
1. Allow signed-in users to upload into their own folder
This follows Supabase's documented example (Storage access control):
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')
);storage.foldername(name) returns the folders in the file path (Supabase: Storage helper functions), so [1] is the first folder.
2. Let them read what they uploaded
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
);The owner_id comparison is Supabase's documented example. Make sure it covers the object being created, which it will for the uploading user.
3. If you use upsert: true, allow updates too
create policy "Users update their own files"
on storage.objects for update
to authenticated
using (bucket_id = 'avatars' and (select auth.jwt() ->> 'sub') = owner_id)
with check (bucket_id = 'avatars' and (storage.foldername(name))[1] = (select auth.jwt() ->> 'sub'));For deletes you need a DELETE policy as well. A Supabase engineer notes in supabase/storage#1241 that delete also needs SELECT.
4. Upload to the path the policy expects
const { data: { user } } = await supabase.auth.getUser()
const { error } = await supabase.storage
.from('avatars')
.upload(`${user!.id}/avatar.png`, file, { upsert: true })5. Don't use the secret key in the browser
Service keys "entirely bypass RLS policies" (Storage access control). That's why AI tools sometimes suggest them as a quick fix. In browser code, that hands your whole storage and database to every visitor. If it's already happened, see Supabase service_role key exposed?
Fix prompt for your AI builder
Paste this into Lovable, Bolt, Cursor, Replit, v0 or whichever tool you build with:
File uploads to Supabase Storage fail with 403 "new row violates row-level security policy". Fix only the storage policies and the upload path:
1. Write a SQL migration with policies on storage.objects for bucket '<BUCKET>': INSERT for authenticated users where (storage.foldername(name))[1] = (select auth.jwt() ->> 'sub'); SELECT where (select auth.jwt() ->> 'sub') = owner_id; UPDATE with the same conditions (we use upsert); DELETE only if the app deletes files.
2. Change the upload code to upload to `${user.id}/<filename>` and make sure the user is signed in before uploading.
3. Do NOT use the service_role or secret key in frontend code, and do not disable RLS.
Show me the SQL before running it, and list the files you changed.How to confirm it's fixed
- Sign in as a test user on the live site and upload a file. It should succeed and appear under
<user id>/in the bucket. - Upload again with the same name (if you use upsert). It should replace the file.
- Sign in as a second test user and confirm they can't read or overwrite the first user's file.
How to find out sooner next time
Heedza doesn't upload files or sign in, so it can't test this flow. It checks that your landing, signup, login and checkout pages load with their forms, and sends a screenshot and fix prompt when one doesn't. One check is relevant here, though: if a "quick fix" puts a Supabase service_role key into your frontend, the free scan flags it as an exposed key.
Let Heedza check this for you. Heedza’s security checks looks for secret keys left in your page code and private files anyone can open. It checks again every time it checks your pages. It never logs in or fills in forms, so it can’t see errors that only show after that.
Made with Lovable, Bolt, Replit, Cursor or v0? Free for 1 site: start free or see prices.