Supabase · 4 min read

Supabase: new row violates row-level security policy.

The bucket exists. The policy looks right. The upload still fails. There are four different causes behind this one error, and each has a quick check.

What you see

A logged in user uploads a file to Supabase Storage, or inserts a row, and gets "new row violates row-level security policy".

The storage bucket exists. You may have written the policy yourself and it looks correct. You can see it in your migration file.

Why it happens

This error means RLS checked the new row and no policy allowed it. That can happen for four different reasons, and only one of them is a wrong policy.

The fix, step by step

Step 1. Check what policies really exist. Do not trust your migration file. Ask the live database:

select policyname, cmd
from pg_policies
where schemaname = 'storage' and tablename = 'objects';

Every policy you expect must be in the result. For a normal table, change the schema and table name.

Step 2. If policies are missing, rerun the migration. Write migrations as idempotent SQL so you can safely rerun them. The service role key cannot run DDL, so use the SQL editor, a Management API token or the database password.

Step 3. Check the policy against the row you are writing. A typical upload policy only allows files in the user's own folder:

create policy "upload to own folder" on storage.objects
  for insert to authenticated
  with check (
    bucket_id = 'uploads'
    and (storage.foldername(name))[1] = auth.uid()::text
  );

With this policy, a path like uploads/USER_ID/photo.jpg passes and uploads/photo.jpg fails. Compare your real upload path to the check.

Step 4. For tables, check the grant too. RLS policies need table grants as well. If the role lacks INSERT, or lacks SELECT when you read the row back, fix that first.

Step 5. If the row can never pass its own check, use a function. Do creation and invite acceptance through SECURITY DEFINER functions instead of loosening the insert policy. Put membership checks in a helper such as is_member(group_id), so the members table policy does not query itself and recurse.

Step 6. If the policy is right and Storage still refuses, test the token. Send the same user token to a normal PostgREST table query. If that works and Storage still fails, the token is not being applied to Storage. Route uploads through a server function that verifies the token, then writes with the service role key:

const { data: { user }, error } = await sb.auth.getUser(bearerToken);
if (error || !user) return { statusCode: 401, body: 'Unauthorized' };

const path = user.id + '/' + fileName;
const { error: upErr } = await admin.storage
  .from('uploads').upload(path, fileBuffer, { contentType });
if (upErr) return { statusCode: 500, body: upErr.message };

Here admin is a client made with the service role key, which stays on the server. The client POSTs the file or a data URL to this function.

Step 7. Check every write. supabase-js returns errors as values instead of throwing. An RLS refusal can even return no error and no row. Chain .select() and confirm a row came back.

How to prevent it

Related traps

Field Notes

This fix is one of 228 in Field Notes: What Broke and How We Fixed It.

Every entry follows the same pattern: the symptom you see, the real cause, the fix that worked, and the rule that stops it coming back. App Store, Google Play, Capacitor, Netlify, Supabase, Stripe, n8n and more.

Get Field Notes

Building a lot? The Builder Bundle has Field Notes plus the launch playbook, the prompt book and the Claude Code skills kit. See the bundle.

Related fixes

Keep reading.