Em resumo
A lista Exposed schemas do Supabase decide que schemas a Data API serve a quem tiver a sua chave pública. Por omissão são public e graphql_public. Cada tabela, vista e função de um schema da lista precisa dos seus próprios grants e de RLS, por isso cada schema extra é mais superfície a defender. Mantenha a lista curta e verifique-a primeiro.
Criei um verificador de segurança do Supabase gratuito, e a lista Exposed schemas é uma das primeiras coisas que ele lê. Quando alguém pergunta se um projeto Supabase é seguro, é por esta definição que começo.
O que significa schema exposto no Supabase?
O Supabase serve a sua base de dados por REST com o PostgREST. O PostgREST só responde pelos schemas da sua lista db_schema, que se edita no Dashboard em Project Settings → Data API → Exposed schemas. Na configuração do projeto que vi, o valor era public,graphql_public, e o guia de schemas personalizados do Supabase diz que o public é exposto por omissão. Um pedido a um schema fora da lista é recusado, e o PostgREST nunca permite pg_catalog nem information_schema nela.
Porque é que a lista importa para a segurança do Supabase?
Tudo o que está num schema exposto é alcançável com a chave pública: tabelas, vistas e funções. O guia de RLS do Supabase é direto: uma tabela num schema exposto sem RLS pode ser lida e escrita por qualquer função com um grant sobre ela. Um schema que não está na lista não pode ser alcançado de todo pela API.
Por isso, um schema extra não é uma alteração neutra. Junte storage ou vault à lista e coloca tabelas internas ao alcance da Data API. Junte o seu próprio schema e cada tabela dele precisa do mesmo cuidado que o public.
Como é que o verificador classifica a lista?
A verificação chama-se extra_exposed_schemas. Tudo o que não for public nem graphql_public conta como extra, e depois:
- Um schema interno do Supabase na lista (
auth,storage,realtime,vault,extensionse semelhantes) é uma falha, gravidade alta. - Só schemas personalizados, por exemplo
api, dão um aviso, gravidade média. - Sem extras, passa.
A mesma lista alimenta outras verificações. Tabelas com RLS desligado, grants de escrita ao anon, políticas sempre verdadeiras, vistas sem security_invoker e funções SECURITY DEFINER que o anon pode chamar só são reportadas para schemas expostos, porque é aí que a chave pública lá chega.
Como usar um schema personalizado em segurança?
Um schema personalizado é um padrão legítimo: o guia Securing your API do Supabase descreve um schema dedicado, como api, como forma de tornar a superfície exposta mais fácil de identificar e auditar, com as tabelas internas em schemas que não são expostos. Dois passos fazem-no funcionar, e o guia de schemas personalizados dá os dois: juntar o schema a Exposed schemas e depois conceder usage sobre ele a anon, authenticated e service_role, mais privilégios sobre os seus objetos.
O exemplo do guia concede ALL em todas as tabelas. Eu não copiaria essa linha tal como está. O guia Securing your API manda dar a cada função o mínimo de que precisa e ativar o RLS em todas as tabelas que expõe. Os grants decidem se uma função pode tocar numa tabela, as políticas decidem que linhas.
Uma alteração à parte: o Supabase deixa de conceder automaticamente à Data API as novas tabelas de public. Passou a ser o padrão nos projetos novos desde 30 de maio de 2026 e será aplicado aos projetos existentes em 30 de outubro de 2026, e as tabelas existentes mantêm os grants. Essa é a camada dos grants. Não muda os schemas que estão na lista.
Como verificar o seu projeto?
Abra Project Settings → Data API e leia Exposed schemas. Se for mais do que public e graphql_public, pergunte para que serve cada schema extra.
Depois liste as tabelas desses schemas que têm o RLS desligado, e se o anon já as pode ler:
select n.nspname as schema,
c.relname as "table",
has_table_privilege('anon', c.oid, 'select') as anon_can_select
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname in ('public', 'api') -- your Exposed schemas
and c.relkind in ('r', 'p')
and not c.relrowsecurity
order by 1, 2;
Corri-a num Postgres 17 descartável, com um schema api que tinha uma tabela com RLS e outra sem, mais uma tabela public sem RLS. Devolveu só as duas sem:
schema | table | anon_can_select
--------+--------+-----------------
api | orders | t
public | logs | t
Cada linha com anon_can_select verdadeiro é uma tabela que qualquer pessoa com a sua chave pública consegue ler, até ativar o RLS ou revogar o grant. A consulta cobre apenas tabelas. As vistas e as funções pedem a sua própria análise, que é o que a minha lista de segurança para apps Lovable percorre, e como testo o Supabase RLS mostra como provar que as políticas funcionam.