Duas chaves idênticas de se olhar, consequências opostas.
Cole uma chave Supabase e descubra qual delas você tem em mãos. Nada é transmitido: a chave é decodificada no seu navegador, porque mandar uma credencial viva de banco de dados para um site checar seria exatamente o erro que esta ferramenta existe para pegar.
O que isso verifica, e como é uma boa resposta
Por que é impossível diferenciar as duas chaves a olho
As duas são JSON Web Tokens. As duas têm o mesmo comprimento, começam com os mesmos caracteres e são copiadas de linhas vizinhas da mesma tela de configurações. A única diferença é uma claim enterrada na seção do meio: role, que diz anon ou service_role. Essa seção é base64, não criptografia, então qualquer um consegue ler. Esta ferramenta só lê para você.
service_role: ignora toda política que você escreveu
Esta chave ignora row level security por completo. Não é "tem permissões amplas": ignora as regras. Toda política que compara uma linha com auth.uid() é pulada, então quem tem a chave lê, edita e apaga cada linha de cada tabela como se as regras nunca tivessem sido escritas. Se ela já apareceu em código de cliente, num repositório público, num print ou num ticket de suporte, trate como já comprometida e rotacione. Coletores automáticos encontram credenciais em bundles públicos em questão de horas.
anon: pública por design, e só tão segura quanto suas políticas
A chave anon foi feita para ir ao navegador, então encontrá-la no seu JavaScript não é um problema. O que ela vale depende inteiramente do seu row level security. Com RLS ativo e políticas que comparam linhas com o usuário logado, ela é exatamente o que deveria ser. Com RLS desligado numa tabela, ou com uma política que diz using (true), essa chave pública lê aquela tabela inteira para qualquer um que abriu seu site, ou seja, para todo mundo.
Como isso dá errado na prática
Quase sempre do mesmo jeito. Alguma coisa devolve um array vazio ou um erro de permissão, o jeito mais rápido de fazer parar é a outra chave, e funciona na hora porque essa chave ignora a regra que estava corretamente te barrando. O erro era o sistema funcionando. Trocar de chave não corrige um problema de acesso, remove o controle de acesso, e em geral vai para produção porque nada quebra visivelmente depois.
A claim de expiração
Chaves do Supabase carregam um exp. Reportamos porque uma chave expirada explica uma classe de falha confusa em produção, mas note que expirar não é remédio para exposição: uma chave service_role que vazou e depois expirou continuou válida durante toda a janela em que esteve pública.
Como corrigir
Se esta é uma chave anon, não há nada a corrigir aqui e o trabalho está nas suas políticas. Se é uma service_role que passou perto de um navegador, rotacione primeiro e investigue depois.
-- A table without this is readable by anyone holding the anon key,
-- which is everyone who has loaded your site.
alter table public.documents enable row level security;
-- Scope every row to its owner. "using" filters reads;
-- "with check" constrains writes. A policy with only "using" lets
-- someone insert rows they will not be able to see afterwards.
create policy "owners read their documents"
on public.documents for select
using (auth.uid() = user_id);
create policy "owners write their documents"
on public.documents for insert
with check (auth.uid() = user_id);-- Run this in the SQL editor. Every row it returns is a table the
-- anon key can read in full.
select tablename
from pg_tables
where schemaname = 'public'
and rowsecurity = false;
-- And this one, for the policy that looks present but grants
-- everything to everyone:
select tablename, policyname, qual
from pg_policies
where schemaname = 'public'
and qual = 'true';anon key
-> client code is fine. VITE_SUPABASE_ANON_KEY,
NEXT_PUBLIC_SUPABASE_ANON_KEY and friends are correct.
service_role key
-> server only. An edge function, a server route, or your host's
secret store. Never in a variable your bundler inlines, which
means never behind NEXT_PUBLIC_, VITE_, PUBLIC_ or REACT_APP_.
# Check what you actually shipped:
grep -rE "service_role|SERVICE_ROLE" ./dist ./build ./.next 2>/dev/null
# And check the history, because deleting a key from a file does
# not delete it from the repository:
git log -p -S 'service_role' -- . | head -501. Supabase dashboard -> Project Settings -> API -> rotate.
2. Update the secret in your host and redeploy the server side.
3. Confirm nothing still reads the old value, then revoke it.
4. Read the logs for the exposure window. A key that was public
for a week deserves a look at what it was used for, not just
a replacement.Isto lê uma chave que você já tem. Não consegue dizer se ela está no seu bundle de produção, se está no seu histórico do git, ou se as tabelas que ela alcança têm políticas que mereçam o nome. Para o primeiro, rode o scanner de chaves de API expostas contra sua URL publicada. Para os outros dois, a resposta está no repositório e não em algo visível de fora.