Início / RLS / Upload no Storage: new row violates RLS · Read in English

Supabase Storage · Correção de erro

Upload no Storage falha: new row violates row-level security policy

Postgres 15 · Supabase Storage✓ código testado num banco de verdade
TL;DR

Todo arquivo é uma linha em storage.objects, e o RLS está ligado lá. Sem uma policy de INSERT que aceite o caminho, o upload é recusado com 403. Crie uma policy de INSERT pro bucket que amarre a primeira pasta ao auth.uid() e faça o upload em <id do usuário>/arquivo. Substituir um arquivo (upsert: true) também exige policies de SELECT e UPDATE.

01 O que é o erro

A API do Storage transforma o erro do Postgres em HTTP 403:

resposta

{ "error": "Unauthorized", "message": "new row violates row-level security policy" }

Um upload insere uma linha em storage.objects (bucket, caminho, dono). Essa tabela tem RLS ligado e, por padrão, nenhuma policy deixa alguém inserir, então todo upload feito pelo cliente é recusado até você criar uma. Deixar o bucket público não muda isso: "público" só vale pra download.

02 A policy: cada usuário sobe na própria pasta

migration.sql

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

storage.foldername(name) devolve as pastas do caminho, então em <id do usuário>/avatar.png a primeira é o id do usuário. O upload precisa usar esse caminho:

upload.ts

const { data: { user } } = await supabase.auth.getUser();
await supabase.storage.from('avatars').upload(`${user.id}/avatar.png`, file);

03 Por que ainda falha

CAUSA 1

O caminho não começa com o id do usuário

Um arquivo na raiz do bucket (avatar.png) não tem pasta, então a checagem dá falso. A pasta de outro usuário também.

CAUSA 2

Usuário não logado

O cliente chama como anon, o auth.uid() vem nulo e uma policy to authenticated não se aplica.

CAUSA 3 · upsert

Substituir arquivo com upsert: true

Sobrescrever um caminho que já existe é um insert que vira update, e o Postgres exige também policies de SELECT e UPDATE. Só com a de INSERT, o upsert é recusado até pra um caminho que ainda não existe.

migration.sql (só se você usa upsert ou lê os arquivos de volta)

create policy "users read their folder" on storage.objects for select to authenticated
  using (bucket_id = 'avatars' and (storage.foldername(name))[1] = (select auth.uid())::text);

create policy "users update their folder" on storage.objects for update to authenticated
  using (bucket_id = 'avatars' and (storage.foldername(name))[1] = (select auth.uid())::text);

04 Prove

✗ sem policy de INSERT  → new row violates row-level security policy
✓ A sobe em A/me.png  → ok
✓ A sobe em B/x.png, na raiz ou em outro bucket  → recusado
✓ upsert só com INSERT, depois INSERT + UPDATE  → recusado; com SELECT também → ok

Não "resolva" com with check (true): isso deixa qualquer um com a sua anon key pública gravar em qualquer lugar do bucket.

◆ GRÁTIS · 30 SEGUNDOS · SEM CADASTRO

Seu app está vazando agora?

Cole a URL do seu app no ar. O scanner lê a página e o JavaScript atrás de uma chave service_role do Supabase, o vazamento que passa por cima de todas as policies deste guia.

◆ GRÁTIS · MIT · npm + GitHub Action

Pegue isso antes de ir pro ar

airlock-rls é um gate de CI que quebra o build quando uma tabela vai pro ar exposta ou uma policy é permissiva: o mesmo tipo de bug, pego no CI contra um banco com as suas migrations aplicadas, antes de chegar em produção.

npx airlock-rls→

Ou comece pelo nextjs-supabase-starter: auth + uma tabela com RLS + um teste de isolamento, pra começar de um padrão que já vem selado.

PDF GRÁTIS

Baixe o cheat sheet de RLS do Supabase

As regras de ouro, as armadilhas que vazam em produção, policies corretas e o teste de isolamento, numa página só (em inglês).

FAQ

Deixei o bucket público. Por que o upload ainda falha?

Público só quer dizer que os arquivos podem ser baixados sem token. Upload, atualização e exclusão continuam passando pelas policies de RLS em storage.objects.

Posso fazer o upload com a chave service_role?

De um servidor, sim: ela ignora o RLS. Nunca do navegador, onde a chave ficaria exposta. Pra upload do usuário, uma policy de INSERT restrita é o caminho certo.