Your upload code looks correct, the user is signed in – yet Supabase Storage responds with:
StorageApiError: new row violates row-level security policy
statusCode: 403
You often already have an INSERT policy – and the upload still fails. Here is the fix most people miss.
The Problem: INSERT Alone Is Not Enough
On upload the Storage API roughly runs INSERT … RETURNING * on storage.objects. Without a matching SELECT policy, Postgres cannot return the new row – the whole transaction fails with RLS/403 even though the insert itself would be allowed.
The Solution: Mirror a SELECT Policy
Step 1: Verify Bucket and Auth
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${user.id}/avatar.png`, file, {
upsert: true,
contentType: file.type,
})
if (error) console.error(error.message, error.statusCode)
The user needs a session (unless the bucket is intentionally public and policies allow anon).
Step 2: Create INSERT and SELECT Policies
-- Uploads into the user's own folder
CREATE POLICY "avatars_insert_own"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- CRITICAL: without SELECT, RETURNING * fails
CREATE POLICY "avatars_select_own"
ON storage.objects FOR SELECT TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- Optional for upsert/update
CREATE POLICY "avatars_update_own"
ON storage.objects FOR UPDATE TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
)
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
Step 3: Path and Policy Must Match
If the policy expects the first folder to be auth.uid(), the upload path must look like:
const path = `${session.user.id}/${file.name}`
await supabase.storage.from('avatars').upload(path, file)
Path avatar.png without a user folder → policy does not match → 403.
Common Mistakes
- INSERT policy only: the classic cause of this exact error.
- Wrong bucket_id string: typo between dashboard bucket and policy.
- Policy for public, upload as authenticated: roles must match the session.
- upsert without UPDATE policy: first upload works, overwrite fails.
Setting Policies in the Dashboard
Storage → Bucket → Policies → New policy. For a quick start use “Allow authenticated uploads to own folder”, then add a matching SELECT policy explicitly.
Troubleshooting Checklist
- ✅ Active session (
getUser())? - ✅ INSERT policy for the bucket present?
- ✅ SELECT policy mirrors the same conditions?
- ✅ Upload path matches the policy (e.g.
{uid}/…)? - ✅ For upsert: UPDATE policy present?
Conclusion
Storage 403 with new row violates row-level security policy is almost always a missing or too-strict SELECT policy on storage.objects – not (only) an insert problem. Mirror the policy, align the path, done.
Additional Resources
- permission denied for table (42501)
- PGRST116: multiple (or no) rows returned
- Edge Functions CORS Error Fix
- How to query users table?
- Supabase Storage File Upload: Complete Secure Guide
- Supabase: infinite recursion detected in policy
- Supabase Auth: Invalid login credentials
- Supabase Edge Functions: Invalid JWT / 401
- RLS insert errors (tables)
- Supabase Storage access control
Upload still failing with 403? Comment with your bucket policy (no secrets) and we can spot the gap.
Comments