Porque RLS (Row Level Security, a regra que decide quais linhas cada usuário enxerga) só entra em ação depois que o banco confere se aquele usuário tem permissão na tabela, e essa permissão é dada pelo comando GRANT. Se o GRANT não existe, a política nunca é avaliada. Dependendo do caminho que a leitura percorre, o resultado aparece como erro ou como lista vazia, e foi a lista vazia que me custou tempo. A solução é escrever o GRANT na migration, junto com a política, em vez de contar com um padrão do projeto. Conferi as fontes em 10 de outubro de 2026.
O que a documentação diz
A documentação do Supabase descreve duas checagens em fila: primeiro os grants, depois as políticas que filtram linhas. Uma requisição sem o grant necessário falha com o código 42501 (permissão negada) antes de qualquer política ser avaliada, e a mensagem típica é permission denied for table. O próprio texto também avisa que adicionar políticas não retira grants que já existam.
A documentação do PostgreSQL diz a mesma coisa de outro jeito: as políticas de segurança por linha existem além do sistema de privilégios do SQL (o GRANT). Com RLS ligado e nenhuma política, vale um default-deny, isto é, nenhuma linha fica visível.
Então são duas camadas com papéis diferentes. O GRANT é o portão da tabela inteira. A política é a régua que escolhe linhas dentro dela. Uma não substitui a outra.
O que vi na prática
Num projeto em que trabalhei, o banco foi recriado a partir das migrations em um projeto Supabase novo. Subiram as tabelas, os dados e todas as políticas. O app abriu com todas as telas vazias, sem mensagem de erro na interface. Nenhuma migration escrevia GRANT.
Dois detalhes tornaram o problema difícil de enxergar:
- O coletor que grava dados (um job de ingestão) usava a chave
service_role. Essa função tem o atributobypassrls, que pula as políticas, mas pular política não é ter privilégio de tabela. A documentação do Supabase diz queservice_roleignora o RLS e não diz que ela dispensa o GRANT. No meu caso, a escrita também parou. - Uma tela continuava funcionando. Ela lia por uma função com
SECURITY DEFINER, que roda com a permissão de quem a criou e não depende do privilégio de quem chama. Por isso aquela página dava a impressão de que o banco estava saudável, enquanto as 16 tabelas estavam inacessíveis para o usuário logado.
Por que isso aparece quando se recria o projeto
Em projetos criados no padrão antigo, o Supabase concede privilégios automaticamente a toda tabela nova no schema public. No projeto que vi, isso vinha de ALTER DEFAULT PRIVILEGES. Esse comando, segundo a documentação do PostgreSQL, define os privilégios de objetos criados no futuro e não mexe nos que já existem. A documentação do Supabase sobre segurança da API diz que a plataforma está mudando o padrão para revogar esses grants automáticos, de modo que expor uma tabela passe a ser uma escolha explícita. Eu medi a diferença no meu projeto: no padrão novo, a configuração de privilégios padrão não incluía SELECT, INSERT, UPDATE nem DELETE para os papéis da API.
O resultado é que um schema que só funcionava porque o projeto de origem dava o GRANT de graça deixa de funcionar quando é recriado em outro. Esse schema não era versionado de verdade: parte do que ele precisava estava fora das migrations.
Sobre o sintoma de tela vazia em vez de erro: a documentação do Supabase afirma que o 42501 é devolvido, e eu vi telas vazias. Não encontrei na documentação uma explicação de por que a interface do meu app engoliu o erro, e isso depende de como cada tela trata a resposta. O que posso afirmar é o que observei.
Como conferir em uma consulta
Esta consulta lista as tabelas do schema public e os privilégios de DML (leitura e escrita) do papel authenticated. Tabela com a coluna vazia está sem acesso:
SELECT t.tablename, string_agg(g.privilege_type, ',') AS dml
FROM pg_tables t
LEFT JOIN information_schema.role_table_grants g
ON g.table_name = t.tablename
AND g.table_schema = 'public'
AND g.grantee = 'authenticated'
WHERE t.schemaname = 'public'
AND (g.privilege_type IS NULL
OR g.privilege_type IN ('SELECT','INSERT','UPDATE','DELETE'))
GROUP BY 1
ORDER BY 2 NULLS FIRST;
Rode essa consulta com um usuário administrador: a information_schema.role_table_grants só mostra os grants que o papel que consulta consegue ver, e não segue herança de papéis.
O que fazer
- Escrever o GRANT na própria migration, ao lado da política, espelhando o que a política pretende. Exemplo para uma tabela
dealslida e editada por usuários logados (o GRANT dá o portão, a política afina):
ALTER TABLE public.deals ENABLE ROW LEVEL SECURITY;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.deals TO authenticated;
-- política mínima de exemplo: em produção, troque o true pelo filtro do seu tenant
CREATE POLICY deals_leitura ON public.deals
FOR SELECT TO authenticated
USING (true);
- Dar privilégio padrão à identidade de ingestão (
service_role) faz sentido. Paraauthenticatedprefiro não dar: assim, toda tabela nova precisa declarar que a interface pode lê-la. - Rodar a consulta acima depois de cada recriação de ambiente, antes de abrir o app.
- Testar o app com um usuário comum logado, e não só com a chave de serviço. A chave de serviço esconde justamente este defeito.
Se você precisa de um sistema com banco, permissões e multi-tenant bem fechados desde o começo, veja como eu trabalho em sistemas e SaaS.
Fontes
- Supabase: grants vêm antes das políticas, erro 42501 e service_role com bypassrls
- Supabase: tabela só é alcançável pela Data API com GRANT, e a mudança do padrão de grants automáticos
- PostgreSQL: row security policies existem além do sistema de GRANT, e default-deny sem política
- PostgreSQL: ALTER DEFAULT PRIVILEGES vale só para objetos criados depois