Início / RLS / Upsert: new row violates RLS · Read in English

Supabase RLS · Correção de erro

Upsert falha: new row violates row-level security policy

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

O upsert() vira insert ... on conflict do update. Pra isso, o Postgres confere as policies de UPDATE e SELECT além da de INSERT, e faz isso mesmo quando a linha é nova. Crie uma policy de UPDATE (com a mesma condição de dono) e uma de SELECT, e dê grant de update.

01 Por que o insert funciona e o upsert não

Por padrão, supabase.from('profiles').upsert(row) é enviado como insert ... on conflict (id) do update. Um comando que pode atualizar precisa de todas as policies que talvez use: INSERT pra linha nova, UPDATE pra linha existente e SELECT pra ler a linha com que conflita. Se a de UPDATE ou a de SELECT faltar, o Postgres recusa o comando logo de cara com new row violates row-level security policy, mesmo quando a linha ainda não existe.

02 A correção

migration.sql

create policy "owner inserts" on profiles for insert to authenticated
  with check (id = (select auth.uid()));

create policy "owner updates" on profiles for update to authenticated
  using (id = (select auth.uid()));

create policy "owner reads" on profiles for select to authenticated
  using (id = (select auth.uid()));

grant select, insert, update on profiles to authenticated;

Mantenha as três com a mesma condição de dono. O usuário continua sem conseguir fazer upsert com o id de outra pessoa: a checagem do INSERT recusa.

03 Se você só insere

Se a linha nunca deve ser sobrescrita, não use upsert(). Use insert() e trate o erro de chave duplicada (23505), ou passe { ignoreDuplicates: true } pra requisição virar on conflict do nothing.

04 Prove

✗ só policy de INSERT, upsert de linha nova  → new row violates row-level security policy
✓ insert comum com a mesma policy  → ok
✗ INSERT + UPDATE, sem SELECT  → continua recusado
✓ INSERT + UPDATE + SELECT  → ok; id de outro usuário → recusado
◆ 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

Por que o Postgres precisa de SELECT num upsert?

Pra detectar o conflito ele precisa olhar a linha existente, e essa leitura passa pelas suas policies de SELECT. Sem nenhuma, o upsert não pode ser checado e é recusado.

Isso vale também pro upload no Storage com upsert: true?

Vale. Substituir um arquivo é um upsert em storage.objects, então ele precisa de policies de SELECT e UPDATE além da de INSERT.