Pular para o conteúdo

Noções de segurança

Supabase "relation does not exist" e a tabela está lá

Supabase "relation does not exist" ou PGRST205: nenhuma tabela tem esse nome nesse schema. Quatro verificações, e o que expor um schema abre.

Vlad Tkachenko12 min de leitura
Um prédio em corte ao lado do poço de um elevador. O elevador está no térreo, e a parada dele um andar acima é um vão tracejado em âmbar.

Em resumo

  • Supabase "relation does not exist" (código do Postgres 42P01) significa que nenhuma tabela ou view com esse nome exato está nos schemas que a consulta percorreu. No seu app, em um projeto atual, o mesmo problema aparece como "Could not find the table in the schema cache" (PGRST205).
  • Confira primeiro o nome letra por letra, maiúsculas incluídas, depois o schema, depois o projeto. Dentro de uma função com search_path vazio, toda tabela precisa do schema na frente: public.invoices.
  • Expor um schema faz o erro sumir e deixa a API alcançar todas as tabelas dele, então confira a Row Level Security de cada uma antes de fazer isso.

Seu builder criou uma tabela invoices. Você a vê no Table Editor, com as linhas e tudo, e a página que deveria listá-las fica vazia. O console do navegador mostra isto:

{
  "code": "PGRST205",
  "details": null,
  "hint": "Perhaps you meant the table 'public.Invoices'",
  "message": "Could not find the table 'public.invoices' in the schema cache"
}

Supabase "relation does not exist" significa que o Postgres não achou nenhuma tabela ou view com esse nome exato nos schemas que a consulta percorreu. O PGRST205 é a mesma resposta dada pelo PostgREST, a parte do Supabase que transforma as chamadas supabase.from(...) do seu app em consultas ao banco, antes mesmo de a consulta chegar ao Postgres.

A solução que circula para esse erro é adicionar o schema em Exposed schemas nas configurações do projeto. Isso resolve uma das quatro causas abaixo. Também deixa qualquer pessoa com a chave pública do seu app alcançar todas as tabelas desse schema, então leia o que isso abre antes de ligar.

O que significa "relation does not exist" no Supabase?

O banco procurou uma tabela com esse nome exato, nos schemas que mandaram ele percorrer, e não achou nenhuma. "Relation" é a palavra do Postgres para qualquer coisa de onde saem linhas, uma tabela ou uma view. O código do erro é 42P01, que o Postgres chama de undefined_table.

Pense no seu banco como um prédio. Cada schema é um andar: public é o térreo, e auth, onde o Supabase guarda seus usuários, é outro. Cada tabela é uma sala com o nome na porta. Uma consulta que dá só o nome de uma sala procura por ela nos andares listados no caminho de busca dela. As requisições do seu app sobem de elevador, e o elevador só para nos andares que seu projeto expõe. O erro quer dizer que ninguém achou uma porta com esse nome nos andares que olhou.

A redação que você vê depende de onde a consulta rodou:

Onde a consulta rodouO que volta
O SQL Editor, uma migration, uma função ou um triggerrelation "invoices" does not exist, código 42P01
Seu app, com PostgREST 13 ou mais novoCould not find the table 'public.invoices' in the schema cache, código PGRST205, HTTP 404
Seu app, com um PostgREST mais antigouma leitura recebe relation "public.invoices" does not exist, HTTP 404, e um insert recebe um erro vazio
Seu app, pedindo um schema que o projeto não expõeInvalid schema: billing, código PGRST106, HTTP 406

O changelog do PostgREST data a redação nova da versão 13.0.0, lançada em 8 de maio de 2025: "Querying non-existent table now returns PGRST205 error". Antes disso, como explicou um dos mantenedores na issue #3697, uma leitura ia para o Postgres sem checagem e voltava como 42P01. O guia do Supabase para proteger a Data API diz que você pode ver a versão do PostgREST do seu projeto no Dashboard. O próprio código do PostgREST define o resto: a dica do PGRST205, quando existe, nomeia a tabela mais parecida que ele conhece no mesmo andar.

Se a mensagem disser permission denied for table, a tabela foi achada e seu app foi barrado na porta. Esse tem outra solução.

Causa 1: o nome na porta tem uma letra maiúscula

O nome precisa bater exatamente, maiúsculas incluídas, e duas das ferramentas que você usa tratam maiúsculas de um jeito diferente da terceira.

A documentação do Postgres resume a regra numa frase: "Quoting an identifier also makes it case-sensitive, whereas unquoted names are always folded to lower case." As outras duas mantêm o que você digitou:

  • o Table Editor do Supabase cria a tabela com exatamente o nome que você dá. O código dele põe o nome entre aspas quando precisa, então Invoices continua Invoices
  • o PostgREST põe entre aspas duplas cada nome que seu app envia (pgFmtIdent), então .from('invoices') pede invoices e mais nada
Como a tabela foi criadaEm SQL, escrevaNo seu app, escreva
create table invoices, ou Invoices digitado em SQL sem aspasinvoices.from('invoices')
create table "Invoices", ou Invoices digitado no Table Editor"Invoices", com aspas.from('Invoices')

A segunda linha produz esse erro pelos dois lados. Seu app chama .from('invoices') e recebe PGRST205, com o nome real na dica, como na mensagem do começo desta página. Ou o app funciona, e uma função ou policy que cita Invoices sem aspas falha com relation "invoices" does not exist, porque o Postgres passou o nome para minúsculas antes de procurar.

Para ver todas as tabelas com esse nome, em qualquer andar e com qualquer combinação de maiúsculas, rode isto no SQL Editor:

select table_schema, table_name
from information_schema.tables
where table_name ilike 'invoices';

A solução que não mexe em nada dos seus dados está no código: escreva "Invoices" com as aspas em SQL, e as maiúsculas exatas no app. Renomear a tabela no lugar disso quebra toda tela que ainda usa o nome antigo. A versão deste erro para colunas tem a mesma armadilha com nomes de coluna.

Causa 2: a tabela está em outro andar

Seu app procura em public a não ser que você diga outra coisa, então uma tabela em qualquer outro schema não existe para ele. É por isso que a mensagem diz public.invoices: public é o andar aonde o elevador foi.

Isso acontece de três jeitos:

  • seu builder criou a tabela num schema próprio, como billing ou private, e o app chama um simples .from('invoices')
  • o schema está escrito dentro do nome, como .from('billing.invoices'). O PostgREST lê a string inteira como um único nome de tabela e procura por ela em public
  • a tabela é auth.users. O guia do Supabase sobre dados de usuários diz: "For security, the Auth schema is not exposed in the auto-generated API." A resposta dele é uma tabela sua em public, profiles no exemplo dele, preenchida por um trigger quando alguém se cadastra, e esse trigger pode falhar do jeito dele

Uma tabela em outro andar é lida com .schema('billing').from('invoices'), e esse schema precisa estar em Exposed schemas nas configurações de API do projeto, como mostra o guia do Supabase sobre schemas próprios. Até lá, a resposta passa a ser PGRST106:

{
  "code": "PGRST106",
  "details": null,
  "hint": "Only the following schemas are exposed: public, graphql_public",
  "message": "Invalid schema: billing"
}

A dica lista os andares em que o elevador para de fato. O PostgREST 12 escreve o mesmo erro de outro jeito, com a lista dentro da própria mensagem: The schema must be one of the following: public, graphql_public. É o texto que o código dele para essa versão produz, e o changelog levou o nome do schema para a mensagem na 13.0.7.

Sua chave secreta sobe no mesmo elevador. A exposição é uma configuração da API, então código de servidor que usa o supabase-js com essa chave recebe o mesmo PGRST106.

O elevador só para nos andares expostos. Uma tabela em outro andar não está em public, e pedir esse andar pelo nome é recusado até ele ser exposto.

O que expor um schema abre?

O elevador passa a parar naquele andar, e com as permissões que o guia do Supabase dá, todo visitante recebe uma chave de cada sala dele. A Row Level Security vira então a única coisa que decide o que cada um pode ler ou mudar.

O guia sobre schemas próprios tem dois passos. O primeiro é a configuração Exposed schemas. O segundo é um bloco de SQL, e as duas primeiras linhas dele são:

GRANT USAGE ON SCHEMA myschema TO anon, authenticated, service_role;
GRANT ALL ON ALL TABLES IN SCHEMA myschema TO anon, authenticated, service_role;

anon é o papel por trás de toda requisição de quem não fez login. ALL é ler, adicionar, mudar e apagar. O bloco continua com ALTER DEFAULT PRIVILEGES, que dá o mesmo acesso a toda tabela criada depois naquele andar.

Isso é seguro onde a Row Level Security, as regras por linha que o Supabase confere a cada requisição, está ligada e escrita para cada tabela. O guia do Supabase para proteger a Data API diz: "Tables created through the Supabase Dashboard have RLS enabled by default. Enable RLS explicitly for tables created in the SQL Editor or through another tool." Um schema que seu builder montou em SQL pode ter tabelas com ela desligada. Exposta com essas permissões, uma tabela assim entrega cada linha a quem tiver sua chave pública, e ainda aceita as mudanças dessa pessoa.

Confira antes de expor:

select tablename, rowsecurity
from pg_tables
where schemaname = 'billing';

Cada linha que diz false é uma tabela sem regra nenhuma. Ligar a Row Level Security em toda tabela mostra como ligar e escrever a primeira regra, e "RLS disabled in public" mostra a regra de leitura aberta que deixa uma tabela tão alcançável quanto antes mesmo com ela ligada.

Você também pode dar menos permissões que o guia. Um andar com uma tabela que só usuários logados leem precisa de duas linhas:

grant usage on schema billing to authenticated;
grant select on billing.invoices to authenticated;

Se suas tabelas respondem a um desconhecido é algo que dá para conferir de fora. Nosso scan gratuito lê seu app no ar com a mesma chave pública que um visitante recebe, mostra quais tabelas devolvem linhas e não pede conta.

Expor um andar com as permissões do guia abre todas as portas dele. A Row Level Security é o cadeado de cada sala, e uma sala sem cadeado está aberta a quem tiver sua chave pública.

Expor o schema ou mover a tabela?

Decida pensando em quem lê a tabela. Uma tabela que seu app lê pelo navegador precisa de uma permissão para o papel que lê e de Row Level Security com uma regra, onde quer que esteja. Mover para public só poupa o passo de expor o schema.

Se for mover:

alter table billing.invoices set schema public;

A tabela mantém as permissões e as regras. Uma função cujo corpo cita billing.invoices passa a falhar com este mesmo erro, porque o corpo guarda o nome do jeito que foi escrito.

Uma tabela de que só o código de servidor precisa pode ficar num andar aonde o elevador nunca vai. Uma função em public pode fazer com ela o único trabalho de que seu app precisa, e chamar uma função do Supabase mostra como tirar o execute de visitantes que não fizeram login.

Causa 3: uma função com caminho de busca vazio

Uma função com search_path = '' não percorre andar nenhum, então toda tabela que ela usa precisa do schema na frente: public.invoices, nunca um invoices sozinho.

O Security Advisor do Supabase pede exatamente essa configuração. O aviso function_search_path_mutable diz: "We recommend pinning functions' search_path to an empty string". A mesma página termina com: "once you set the search_path = '' all references to tables/functions/views/etc in your function's body must be qualified with a schema name."

Então o aviso se resolve com uma linha, e a função passa a falhar no primeiro nome de tabela sem schema:

create or replace function public.count_invoices()
returns bigint
language plpgsql
set search_path = ''
as $$
begin
  return (select count(*) from invoices);  -- relation "invoices" does not exist
end;
$$;

Escreva from public.invoices e ela volta a funcionar. Mantenha a configuração: ela existe para que quem chama a função não consiga apontar um nome sem schema para uma tabela própria.

Chamada do seu app com .rpc(), o erro atravessa o PostgREST como 42P01 com HTTP 404, o status que o código do PostgREST dá a uma tabela desconhecida. Um trigger de novos cadastros que falha assim aparece como "Database error saving new user".

Com o caminho de busca vazio não há andar nenhum para percorrer, então só um endereço completo encontra a sala.

Causa 4: a tabela ainda não está nesse projeto

A tabela existe em outro banco, ou só no seu código.

Uma conta do Supabase pode ter vários projetos, e uma branch é um projeto separado com o próprio banco. Uma tabela criada no SQL Editor do projeto errado é uma sala em outro prédio. A versão deste erro para colunas mostra como comparar a referência do projeto no endereço do Supabase do seu app com a do dashboard.

Se você constrói com Lovable, volte no chat procurando um SQL que pediram para você aprovar. A documentação de Supabase do Lovable diz que ele "writes the SQL, shows it to you, and asks for your approval in the project chat before running it". Enquanto você não aprova, a tabela só existe no código que a lê.

Mover uma tabela de que seu app depende

Guarde uma cópia do banco de antes da mudança, e fique sabendo quando algo que seu app expõe mudar.

Reeve Care guarda uma cópia do seu banco no Supabase:

  • uma cópia criptografada num cronograma, no armazenamento próprio da Reeve na UE, fora da sua conta do Supabase
  • cada schema que você criou, public e os outros, com as tabelas e linhas como estavam antes da sua mudança
  • cada cópia verificada antes de valer, contando as linhas de cada tabela
  • seus arquivos enviados também, assim que você conecta uma credencial do Storage
  • uma restauração com um clique, com uma cópia de segurança do estado atual feita antes
  • tudo o que o Monitor faz

Reeve Monitor continua conferindo de fora:

  • se seu app está no ar, conferido a cada minuto
  • a verificação de segurança completa a cada hora, em até três apps
  • uma mensagem quando um resultado muda, para que um deploy que muda o que seu app expõe chegue até você sem você ter que ir olhar
  • um relatório mensal, em linguagem simples, do que ele viu

O que fazer agora

O que fazer

  • Leia a mensagem: o nome entre aspas é o que foi pedido, e public. na frente é o schema que foi percorrido.
  • Compare o nome com o Table Editor, maiúsculas incluídas. Uma tabela com maiúscula no nome precisa de aspas duplas em SQL.
  • Descubra em que schema a tabela está com a consulta em information_schema.tables, e chame uma tabela fora de public com .schema('nome').from(...).
  • Antes de expor um schema, rode a consulta em pg_tables e ligue a Row Level Security em cada tabela que disser false.
  • Dentro de uma função com search_path = '', escreva cada tabela como public.nome.
  • Confira se o projeto que seu app chama é aquele em que você criou a tabela.

Comece pelos dois nomes da mensagem, a tabela e o schema na frente dela, porque eles dizem exatamente qual porta foi pedida e em que andar. Antes de mover, renomear ou expor algo que as pessoas usam, guarde uma cópia do banco de antes da mudança.

Perguntas frequentes

Por que o Supabase diz que minha tabela não existe?

Porque nenhuma tabela com esse nome exato está no schema que a consulta percorreu. Confira quatro coisas, nesta ordem: as maiúsculas do nome, o schema onde a tabela está (seu app procura em public a não ser que você diga outra coisa), o projeto com que seu app fala, e se a consulta roda dentro de uma função com search_path vazio, onde toda tabela precisa do schema na frente.

O que significa "the schema must be one of the following"?

É o erro PGRST106 com a redação das versões antigas do PostgREST. Seu app pediu um schema que o projeto não expõe, por exemplo com .schema('nome'), e a lista que vem depois são os schemas expostos. As versões novas dizem "Invalid schema: nome" e põem a mesma lista na dica. Expor o schema resolve, e vale decidir isso com calma.

É seguro expor um schema no Supabase?

Só quando cada tabela dele tem a Row Level Security ligada e uma regra escrita. O guia do Supabase sobre schemas próprios dá a anon, o papel de todo visitante que não fez login, acesso total a cada tabela do schema, inclusive às criadas depois. O guia de segurança dele acrescenta que tabelas criadas no SQL Editor não vêm com Row Level Security ligada.

Maiúsculas fazem diferença no nome de uma tabela do Supabase?

Sim. Um nome escrito em SQL sem aspas duplas vira minúsculo, enquanto o Table Editor e o supabase-js mantêm as maiúsculas exatamente como foram escritas. Uma tabela criada como Invoices se escreve "Invoices", com aspas, em SQL, e .from('Invoices') no seu app.

Devo mover a tabela para public?

Mover resolve a metade do erro que é sobre o schema e nada da segurança: uma tabela que seu app lê pelo navegador precisa de uma permissão para o papel que lê e de Row Level Security com uma regra, onde quer que esteja. Uma tabela de que só o código de servidor precisa pode ficar em um schema não exposto, alcançada por uma função em public que faz o único trabalho de que seu app precisa.

Escrito por

Vlad Tkachenko

Fundador da Reeve

Passo meus dias olhando apps criados com Lovable, Bolt, v0, Cursor e Replit, e a curta lista de erros que aparecem neles sem parar.

Mais sobre o autor

Leia depois

Todos os artigos

Faça backup do seu banco Supabase antes da próxima mudança

O Reeve guarda uma cópia criptografada em horários programados e a restaura para você se algo quebrar.

Ver backups do Supabase

Verificação externa automatizada, não uma auditoria completa. A ausência de achados não é garantia de segurança.