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.

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 rodou | O que volta |
|---|---|
| O SQL Editor, uma migration, uma função ou um trigger | relation "invoices" does not exist, código 42P01 |
| Seu app, com PostgREST 13 ou mais novo | Could not find the table 'public.invoices' in the schema cache, código PGRST205, HTTP 404 |
| Seu app, com um PostgREST mais antigo | uma 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õe | Invalid 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
InvoicescontinuaInvoices - o PostgREST põe entre aspas duplas cada nome que seu app envia
(
pgFmtIdent), então.from('invoices')pedeinvoicese mais nada
| Como a tabela foi criada | Em SQL, escreva | No seu app, escreva |
|---|---|---|
create table invoices, ou Invoices digitado em SQL sem aspas | invoices | .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
billingouprivate, 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 empublic - 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 empublic,profilesno 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 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 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".
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,
publice 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 depubliccom.schema('nome').from(...). - Antes de expor um schema, rode a consulta em
pg_tablese ligue a Row Level Security em cada tabela que disserfalse. - Dentro de uma função com
search_path = '', escreva cada tabela comopublic.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.