Supabase Storage: Fix 403 "new row violates row-level security policy" on Upload

7. August 2026 approx. 4 min read Supabase
Contents 10

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

  1. ✅ Active session (getUser())?
  2. ✅ INSERT policy for the bucket present?
  3. ✅ SELECT policy mirrors the same conditions?
  4. ✅ Upload path matches the policy (e.g. {uid}/…)?
  5. ✅ 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

Upload still failing with 403? Comment with your bucket policy (no secrets) and we can spot the gap.

Comments