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
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
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.
Usuário não logado
O cliente chama como anon, o auth.uid() vem nulo e uma policy to authenticated não se aplica.
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
A/me.png → okB/x.png, na raiz ou em outro bucket → recusadoNão "resolva" com with check (true): isso deixa qualquer um com a sua anon key pública gravar em qualquer lugar do bucket.
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.
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.
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.
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.