Noções de segurança
"Infinite recursion detected in policy" sem desligar o RLS
"Infinite recursion detected in policy for relation" significa que a sua policy perguntou à tabela que ela protege. Veja como quebrar o círculo.

Em resumo
- "Infinite recursion detected in policy for relation" é o erro 42P17 do Postgres. Uma regra de Row Level Security numa tabela foi fazer uma pergunta a essa mesma tabela, e a pergunta não tem fim.
- Não é sinal de que Row Level Security era errado para o seu app. É sinal de que uma regra perguntou em círculo.
- Uma função auxiliar pequena lê a tabela fora das regras e quebra o círculo. Desligar Row Level Security nessa tabela também faz o erro parar, entregando cada linha dela a quem tiver a chave que viaja dentro do seu app.
Você ligou o Row Level Security, escreveu uma regra para que admins vejam todos os perfis e cada um veja só o seu, e agora nenhuma tela do seu app carrega. Toda leitura volta com a mesma linha:
infinite recursion detected in policy for relation "profiles"
A maioria das respostas que você vai achar diz a mesma coisa, e é a coisa errada: desligue o Row Level Security nessa tabela até resolver. O erro realmente para. O que faz ele parar é que a tabela agora pode ser lida por qualquer um que visite o seu site.
O que significa "infinite recursion detected in policy for relation"
Uma regra numa tabela precisou ler essa mesma tabela antes de conseguir responder.
Imagine um porteiro que confere cada visitante numa lista de convidados, e a lista fica dentro da sala que ele está guardando. Para ler a lista ele tem que entrar. Para entrar ele tem que conferir a lista. Não existe primeiro passo, então ele fica no corredor e ninguém vai a lugar nenhum.
Foi isso que aconteceu com o seu banco. Pediram algumas linhas de profiles,
ele foi até a regra de profiles para descobrir quais você podia ver, e a regra
mandou ele olhar em profiles. O Postgres percebe que está andando em círculo,
desiste e levanta o erro com o código 42P17 junto. A Supabase repassa isso
intacto para o seu app, e é por isso que parece linguagem de máquina.
A consulta falha para todo mundo a quem a regra se aplica, então isso costuma chegar como um app inteiro ficando em branco de uma vez, e não como uma tela quebrada.
O círculo, desenhado
A regra que provoca isso é a que quase todo mundo escreve primeiro, porque ela diz exatamente o que você quer dizer:
-- Rejeitar: isto lê profiles para decidir quem pode ler profiles.
create policy "admins read every profile" on profiles for select
to authenticated
using (
exists (
select 1 from profiles
where id = (select auth.uid()) and role = 'admin'
)
);
O exists (select 1 from profiles …) é o problema inteiro. Ler profiles
significa aplicar a regra de profiles, que roda exists (select 1 from profiles …) de novo.
O próprio guia de Row Level Security da Supabase nomeia a versão com duas tabelas: policies em duas tabelas que leem uma a outra nunca se resolvem, e a consulta falha para cada papel a que as policies se aplicam. Uma tabela lendo a si mesma é a mesma forma sem o desvio.
Por que isso sempre acontece na tabela profiles?
Porque é em profiles que fica quem é essa pessoa.
Qualquer regra que trate um tipo de pessoa diferente de outro precisa descobrir
de que tipo é quem está chamando, e esse dado mora numa coluna de alguma tabela.
Seja qual for o nome dessa tabela, a regra que protege ela acaba lendo ela. Num
app feito com Lovable, Bolt, v0 ou Replit costuma ser profiles, porque é ali
que vai parar a linha de cada pessoa logada. Uma regra sobre times, organizações
ou documentos compartilhados produz o mesmo círculo na tabela que guarda a
participação.
Então a recursão sai de escrever a regra óbvia sobre a única tabela que precisa responder uma pergunta sobre si mesma.
O conserto: um auxiliar que lê a tabela fora das regras
Mude a pergunta para uma função pequena que roda como dona do banco, para que
ler profiles e responder não passe pela regra de profiles.
create schema if not exists private;
create function private.is_admin()
returns boolean
language sql
security definer -- roda como o papel que a criou
set search_path = '' -- assim todo nome lá dentro precisa vir por extenso
stable
as $$
select exists (
select 1 from public.profiles
where id = (select auth.uid()) and role = 'admin'
);
$$;
revoke execute on function private.is_admin() from public;
grant usage on schema private to authenticated;
grant execute on function private.is_admin() to authenticated;
-- A regra agora pergunta para a função em vez da tabela.
create policy "admins read every profile" on profiles for select
to authenticated
using ( (select private.is_admin()) );
Agora o porteiro tem a lista de convidados numa mesa do corredor. Ele lê sem abrir a porta, e a porta continua fechada para todos que não estão nela.
Quatro linhas ali fazem um trabalho específico, e três delas são a diferença entre um conserto e um buraco novo:
security defineré o que quebra o círculo. O guia da Supabase define isso como uma função que roda sob o mesmo papel que criou a função, e na Supabase esse papel é opostgres, que pode ler por cima das regras.set search_path = ''cabe em cada uma delas. A orientação da Supabase é colocar isso em toda função security definer e escrever os nomes de tabela por extenso, porque sem umsearch_pathfixado quem chama pode apontar um nome não qualificado para um objeto próprio e rodá-lo com os privilégios do dono da função. É por isso que a função escrevepublic.profilespor extenso.- O schema
privateimporta pelo mesmo motivo. O guia da Supabase avisa que uma função security definer num schema exposto pode ser chamada pela Data API com os privilégios de quem a criou, e diz para nunca criar uma num schema listado em Exposed schemas nas configurações da sua API. (select private.is_admin()), embrulhado num select próprio. Isso deixa o Postgres calcular a resposta uma vez para o comando inteiro em vez de uma vez por linha, que é o que a Supabase documenta como motivo para embrulharauth.uid()do mesmo jeito.
As linhas revoke e grant mantêm a função chamável pelos usuários logados e
por mais ninguém.
Essa policy cobre a leitura. Se salvar começar a falhar assim que as suas telas voltarem a encher, você encontrou outra mensagem com outra causa.
O conserto que não conserta
Desligar o Row Level Security em profiles também faz o erro parar, num clique,
e entrega cada linha dessa tabela a quem tiver a chave que o seu app envia ao
navegador.
Essa chave é feita para ser pública. Ela está no código do seu site, e qualquer pessoa consegue tirá-la de lá em alguns segundos. O que estava impedindo ela de devolver a sua tabela de usuários inteira eram as regras que você acabou de desligar.
O seu app fica com a mesma cara depois nos dois casos, e é essa parte que faz isso ficar. Nada nas suas telas diz qual das duas você escolheu, e o erro sumiu nas duas.
O que diz é o seu banco, perguntado de fora. Escaneamos 31.056 apps no ar feitos com Lovable, Bolt, v0, Replit e os outros, e vimos um projeto Supabase atrás de 8.435 deles. Foi isto que a verificação de Row Level Security encontrou:
| O que perguntamos | Apps |
|---|---|
| Tinham um projeto Supabase visível | 8.435 |
| Deu para perguntar | 3.680 |
| Devolveram linhas para um pedido sem login | 2.096 |
| Dessas, de uma tabela de pessoas, pedidos ou mensagens | 394 |
A linha do meio é a honesta. Em 4.755 desses apps a verificação não obteve resposta, e dizemos isso em vez de dá-los como limpos. E algumas das 2.096 são para serem lidas mesmo, porque uma tabela pública é uma coisa que se quer de verdade; de fora, um cardápio e uma lista de sócios são iguais.
Não dá para dizer quantas dessas tabelas foram abertas por alguém tirando da frente exatamente este erro. Dá para dizer que uma tabela aberta é o fim comum do conselho que aparece no topo dos resultados de busca.
Se você prefere não ter que pensar nisso, o nosso scan gratuito lê o seu site no ar e diz quais das suas tabelas respondem a um estranho. Leva cerca de 20 segundos e não pede conta: escaneie o seu app.
Mais uma coisa acontece quando você desliga. O próprio advisor da Supabase começa a reportar a tabela, que é o aviso RLS disabled in public, e esse aviso vai continuar lá daqui a um mês.
Como saber se o círculo foi mesmo embora
O erro parar não é o teste, porque duas coisas diferentes fazem ele parar.
Existe ainda um jeito de a função auxiliar parecer certa e continuar recursiva,
e a Supabase documenta: uma função security definer só pula as regras se o dono
dela puder. Na Supabase o dono é o postgres, que tem esse privilégio, então o
padrão acima funciona. Uma função que pertença a um papel sem ele, ou que leia
uma tabela com force row level security, volta a passar pela regra e continua
no círculo.
Duas conferidas, e faça as duas:
Escreva os casos e rode. O procedimento da Supabase é um arquivo .sql em
supabase/tests/ afirmando permissão e recusa para cada operação, para um
usuário logado e para um anônimo, rodado com supabase test db. Um admin tem
que ver todos os perfis, um membro o dele, um estranho nada. Até isso passar, o
que você sabe é que o erro parou.
Depois pergunte de fora, sem login. Essa é a pergunta que o navegador de um visitante faz, e a única que reflete o que o seu app realmente entrega. Se um estranho consegue ler o seu banco mostra o caminho, e Row Level Security pode estar ligado e a tabela continuar pública cobre o caso em que as regras existem e deixam todo mundo passar assim mesmo.
Quando a recursão está dizendo que o schema está errado
Às vezes não há pergunta circular para tirar, porque as duas tabelas dependem mesmo uma da outra.
O guia da Supabase usa compartilhamento como exemplo: uma regra em lists
confere list_members para ver com quem a lista foi compartilhada, e uma regra
em list_members confere lists para ver de quem ela é. A regra de cada tabela
lê a outra, e nenhuma pode ir primeiro. O mesmo auxiliar desfaz isso, com uma
função que devolve os ids das listas a que quem chama pertence, e as duas regras
perguntando para essa função em vez de perguntarem uma à outra.
O sinal que vale notar é quando você precisa de três ou quatro dessas para um schema funcionar. A essa altura a participação está sendo reconstruída dentro das regras toda vez, e uma única tabela que diga claramente quem pode ver o quê costuma dar menos manutenção do que as regras que você está desembaraçando.
Como manter a tabela fechada depois de consertar
Uma regra que você consertou hoje descreve o banco de hoje. A próxima policy, a próxima tabela e a próxima vez que alguém tira um erro da frente à meia-noite acontecem todas depois da última vez que você olhou, e nenhuma delas muda nada que você consiga ver nas suas telas.
O Reeve Monitor faz ao seu app as mesmas perguntas de novo, com horário:
- as nove verificações a cada hora, em até três apps
- uma mensagem quando um resultado muda, para que uma tabela que abriu de madrugada não espere você notar
- se o app responde, a cada 60 segundos
- um relatório mensal do que ele viu
Se o seu app guarda os dados no seu próprio projeto Supabase, o Reeve Care ainda guarda uma cópia desse banco. Uma regra que deixa um estranho ler é um problema. Uma regra que deixa um estranho escrever é o outro, e nenhuma verificação deste artigo traz linhas apagadas de volta.
- uma cópia criptografada do seu banco Supabase toda noite, guardada onde o seu projeto não alcança
- cada cópia verificada antes de contar, contando as linhas de cada tabela
- uma restauração em um clique quando você precisar
- os seus arquivos enviados também, assim que você conectar uma credencial de Storage
- tudo o que o Monitor faz
O que fazer agora
O que fazer
- Deixe o Row Level Security ligado. O erro é sobre uma regra, e desligar as regras é uma mudança na tabela inteira.
- Mude a conferência de papel para uma função
security definernum schema que não esteja exposto, comset search_path = ''e cada nome de tabela escrito por extenso. - Aponte a policy para a função, embrulhada como
(select private.is_admin()), para rodar uma vez por comando e não uma vez por linha. - Escreva os casos permitidos e negados em
supabase/tests/e rodesupabase test db: um admin vê todos os perfis, um membro o dele, um estranho nada. - Se você já desligou o Row Level Security para destravar, religue hoje e conserte a regra direito. O advisor da Supabase vai continuar reportando essa tabela até lá, e ele cabe em todas as tabelas que você tem.
Se você quer o resto da lista para um app recém-lançado, a checklist de segurança de 10 minutos cobre isto e as outras coisas que vale fechar antes de alguém encontrar.
Perguntas frequentes
O que causa "infinite recursion detected in policy for relation"?
Uma regra numa tabela que precisa ler essa mesma tabela antes de conseguir responder. A sua regra diz mais ou menos "deixe esta pessoa passar se a linha dela em profiles disser que é admin", então o banco vai ler essa linha em profiles, o que obriga a conferir a regra de profiles, que o manda ler a linha de novo. O Postgres percebe que está andando em círculo, para e levanta o erro 42P17. A mesma coisa acontece entre duas tabelas cujas regras leem uma a outra.
É seguro usar uma função security definer numa policy?
É, com duas condições que o próprio guia de Row Level Security da Supabase enuncia. Coloque search_path como string vazia e escreva cada nome de tabela por extenso lá dentro, ou seja public.profiles e não profiles. Sem isso, alguém pode apontar um nome não qualificado para um objeto próprio e fazê-lo rodar com os privilégios do dono da função. E crie a função num schema que não esteja exposto na API, porque uma função security definer num schema exposto pode ser chamada de fora com os privilégios de quem a criou.
Devo desligar o RLS para resolver?
Isso faz o erro parar e abre a tabela. Com Row Level Security desligado, cada linha dessa tabela pode ser lida por qualquer um que tenha a chave que o seu app envia ao navegador, e essa chave qualquer pessoa consegue tirar do seu site. O seu app se comporta igual nos dois casos, então depois nada diz qual das duas você escolheu. A função auxiliar logo abaixo faz o erro parar e mantém a tabela fechada.
Por que isso sempre acontece na tabela profiles?
Porque é em profiles que costuma ficar a resposta para "quem é esta pessoa". Uma regra que trata admins de um jeito, ou membros de um time de outro, precisa saber o que é quem está chamando, e esse dado está guardado em profiles. Então a regra que protege profiles acaba lendo profiles. Qualquer tabela que guarde o papel ou a participação de quem chama pode produzir isso, e num app feito com Lovable, Bolt ou v0 essa tabela costuma ser a chamada profiles.
Como eu confiro se a minha policy funciona mesmo?
Duas coisas diferentes fazem o erro parar, então o erro sumir sozinho diz muito pouco. Escreva os casos permitidos e negados num arquivo .sql em supabase/tests/ e rode supabase test db, que é o procedimento que a Supabase publica junto com o guia: um dono logado precisa passar e um estranho precisa ser recusado. Depois faça a mesma pergunta ao seu banco de fora, sem login nenhum, do jeito que o navegador de um visitante faria.