Your app tries to upload a file to Supabase Storage, and the request fails with one of these:
new row violates row-level security policy
StorageApiError: new row violates row-level security policy
Unauthorized
Supabase Storage refuses every upload until the storage.objects table has policies that allow it. The fix: add an INSERT policy for the bucket, scoped to the user's own folder, and add SELECT and UPDATE policies if the app overwrites files with upsert.
I see this one a lot in apps built with Lovable and Bolt on Supabase, usually the first time someone adds an avatar or document upload.
Why it happens
-
No policy on
storage.objects. Supabase's docs say it plainly: "By default Storage does not allow any uploads to buckets without RLS policies." A new bucket starts closed. -
Upsert needs more than INSERT. Overwriting an existing file (
upsert: true) also needs SELECT and UPDATE policies. - The path doesn't match the policy. The policy expects files in a folder named after the user, but the app uploads to the bucket root or to a different folder.
- The upload runs signed out. A policy that compares against the user's id fails for a request with no session.
Find the cause
- Copy the exact error and note the bucket and the path the app uploads to.
- In the Supabase dashboard, open Storage > Policies and list the policies for that bucket by operation.
- Check whether the upload call passes
upsert: true. - Confirm a session exists at the moment of upload.
Fix 1: let users upload into their own folder
Upload files to a path that starts with the user's id, for example avatars/<user-id>/photo.png. Then create an INSERT policy for that bucket that checks the first folder against the signed-in user, plus a SELECT policy so they can read their own files:
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 );
The sub claim in the JWT is the signed-in user's id, which is what Supabase's own examples use to match a user to their folder.
Fix 2: allow overwrites only where needed
If the app replaces files, add an UPDATE policy with the same folder check, alongside the SELECT policy:
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 it doesn't need to replace files, upload with a new file name each time and leave upsert off. Fewer policies, fewer surprises.
Check the fix worked
- Signed in as user A, upload a file: it succeeds and lands in A's folder.
- Still as user A, try to upload into B's folder: it fails.
- Signed out, try to upload: it fails.
All three matter. A policy that lets everything through also makes the first test pass.
Stop it coming back
- Decide per bucket whether files are public or private before you build the upload screen.
- Never upload from the browser with a secret or service key. It bypasses every policy.
Quick answers
Should the bucket be public? Only for files anyone may see, such as product images. Profile documents, invoices and user uploads belong in a private bucket with policies.
Why the JWT sub claim? It is the signed-in user's id, which Supabase's own examples use to match a user to their folder.
Source: Supabase: Storage access control.
I first published this fix on our site: Supabase Storage upload "new row violates RLS": fix with SQL. To check your live app for exposed keys, missing security headers and broken basics in about a minute, run the free Launch Check.
I wrote this with AI assistance and reviewed it myself.
Top comments (0)