Pular para o conteúdo

Noções de segurança

"No API key found in request" no Supabase, e a correção errada

"No API key found in request" significa que sua requisição chegou ao Supabase sem chave. Quase toda resposta aponta para as regras do seu banco.

Vlad Tkachenko11 min de leitura
Uma requisição chega a um portão alto com a fenda do crachá vazia e uma barra atravessada, e o banco de dados a que ela ia fica intacto atrás.

Em resumo

  • "No API key found in request" significa que sua requisição chegou ao Supabase sem o cabeçalho apikey, então foi recusada na porta antes que o seu banco de dados fosse consultado.
  • A correção certa é sua chave publicável nesse cabeçalho, que é a chave feita para ser pública. A biblioteca cliente do Supabase a envia por você em toda requisição.
  • As duas correções a que as pessoas recorrem no lugar dela são a chave secreta e uma regra mais frouxa na tabela. As duas calam a mensagem, e as duas deixam suas linhas legíveis para qualquer um que tenha a chave que viaja dentro do seu app.

Ontem seu app funcionava. Hoje uma lista que costumava encher de linhas está vazia, e se você abre o console do navegador há uma resposta curta do Supabase onde deveriam estar os dados:

{
  "message": "No API key found in request",
  "hint": "No `apikey` request header or url param was found."
}

É tudo o que você recebe. Não diz qual tabela você estava lendo, o que você enviou, nem o que mudar.

Aqui está a parte que guia após guia conta errado: "No API key found in request" fala de um cabeçalho que faltou, e quase todo conselho que você vai encontrar fala das regras do seu banco de dados. No fórum de discussão do Supabase essa mensagem tem um tópico só dela, dezesseis pessoas propõem ali oito remédios diferentes, e três deles mandam adicionar ou afrouxar uma regra em alguma das suas tabelas. Todos os que tentaram contam que a mensagem parou. Se uma regra algum dia foi a causa é outra pergunta, e o tópico nunca a resolve. O que é certo depois é que mais gente pode ler a tabela do que antes.

O que significa "No API key found in request"

O Supabase recusou sua requisição na porta da rua, antes de perguntar qualquer coisa ao seu banco de dados.

Toda requisição ao seu projeto chega primeiro a uma porta que o Supabase coloca na frente de tudo o mais. Naquele momento a função dela é estreita: ler o cabeçalho apikey, comparar o valor com as chaves do seu projeto e deixar a requisição passar. Sem chave para ler, ela para ali e responde 401, o código de estado para "não sei quem você é". A própria documentação do Supabase sobre essa porta diz que uma chave ausente ou inválida é recusada assim.

Pense nessa porta como um porteiro na entrada do prédio, e não como uma fechadura em um arquivo. O porteiro pede um crachá a todo mundo que chega. As regras que decidem quem pode abrir qual gaveta ficam dois andares acima, e aqui ninguém as consultou, porque nada passou da entrada.

Como erro, esse é brando. Nada foi lido, nada foi escrito, e seus dados estão exatamente como estavam. Uma requisição foi recusada.

A porta lê a chave. As regras da sua tabela ficam uma parada adiante, e uma requisição recusada nunca chega até elas.

Por que minha requisição chegou sem chave?

Porque o valor que seu app deveria enviar não estava na requisição que o navegador realmente fez. Quatro causas cobrem quase todos os casos.

A chave nunca chegou ao app compilado. Seu código a lê de uma variável de ambiente, o build que gerou seu site no ar não tinha essa variável definida, e o createClient recebeu uma string vazia. Tudo compila, o app carrega, e cada requisição sai sem chave. Em um projeto Vite ou Next a variável ainda precisa carregar o prefixo que a marca como segura para o navegador, o que é uma armadilha à parte.

Você está chamando o caminho REST na mão. Um fetch escrito contra /rest/v1/sua_tabela envia os cabeçalhos que você digitou e mais nada. A biblioteca cliente do Supabase adiciona apikey por você; uma requisição escrita na mão tem o que você deu a ela.

Alguma coisa no meio tirou o cabeçalho. Uma regra de rewrite, um proxy ou um gateway seu fica entre o app e o Supabase e repassa a requisição sem ele.

Você está olhando um redirecionamento e não uma requisição de dados. Se a mensagem aparece depois que alguém faz login ou clica num link de confirmação, e a barra de endereço ainda mostra sua URL do Supabase, a configuração a olhar é a Site URL dentro de Auth. A resposta com mais reações de todo aquele tópico do Supabase é de alguém que tinha escrito ali o endereço do próprio site sem o https:// na frente.

A correção, em uma linha

Envie sua chave publicável no cabeçalho apikey.

import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  'https://seu-projeto.supabase.co',
  'sb_publishable_…',
)

Crie o cliente assim e a biblioteca coloca a chave no cabeçalho certo em toda requisição, de modo que você nunca escreve o cabeçalho. A documentação do Supabase é específica sobre qual é: chaves publicáveis e secretas viajam em apikey e não em Authorization: Bearer, porque são strings curtas e opacas e qualquer coisa que tente verificá-las como um JWT falha.

Essa chave deve mesmo estar no seu app. Ela diz a que projeto uma requisição pertence, e o que um visitante com ela alcança é decidido pelas regras das suas tabelas, que é a razão inteira de ela poder ser pública. Quais chaves de API são seguras no frontend percorre o resto da família.

A correção que piora tudo

A chave secreta entra no mesmo cabeçalho, e num projeto antigo ela cala a mensagem e segue funcionando.

Isso é um único clique errado. Seu painel lista as duas chaves na mesma página, e o par antigo se parece: mesmo formato, mesmo comprimento, uma embaixo da outra. O tamanho do estrago depende de qual par o seu projeto emite.

Uma chave secreta nova é recusada no navegador. O Supabase bloqueia sb_secret_… olhando o cabeçalho User-Agent e responde 401. Colar uma delas no frontend, portanto, não faz o erro sumir, que é o melhor desfecho disponível aqui.

Uma chave service_role antiga não tem esse bloqueio. Ela funciona. A lista enche, o app se comporta igual à semana passada, e a chave agora está num arquivo que qualquer visitante pode baixar. Ali ela ignora toda regra que você escreveu: service_role carrega o atributo BYPASSRLS do Postgres, então uma policy nunca se aplica a ela. Cada linha de cada tabela, legível e gravável por quem abrir o arquivo.

A nota do Supabase sobre o bloqueio do navegador merece duas leituras. O bloqueio responde 401, e um atacante ainda consegue usar a chave com outras ferramentas. O que ele protege são as requisições do seu próprio app; a mesma chave no mesmo arquivo responde a uma requisição enviada de um script.

O bloqueio do navegador é sobre o navegador. A mesma chave no mesmo arquivo funciona a partir de qualquer coisa que não seja um.

O outro caminho errado, e por que ele é tão fácil

Afrouxar uma regra na tabela também cala a mensagem, e muda quem pode ler suas linhas.

Aqui está aquele tópico do Supabase inteiro, agrupado pelo que cada resposta pede que você mude:

O que mudarQuantas das oito respostasO que isso muda de verdade
A Site URL dentro de Auth1Onde um redirect aterrissa
Uma policy ou um grant3Quem lê e escreve suas linhas
A consulta ou a sessão3Seu próprio código
O cabeçalho apikey1O que a porta estava pedindo

A última linha é a que responde à dica contida na mensagem. É o comentário do pé do tópico e tem um voto só.

Nenhuma das outras sete foi escrita de má-fé, e é isso que torna a coisa difícil. Uma mesma mensagem aparece mesmo em várias situações, quem respondeu de fato colocou o próprio app no ar, e uma policy que já faltava é mesmo algo a consertar. O que sai errado é a ordem: reescreve-se uma regra para calar uma mensagem sobre um cabeçalho, e a regra é a metade desse par que ninguém confere depois. Seu app funciona dos dois jeitos, então nada lhe diz qual dos dois você fez.

De fora, o resultado aparece. Nosso scanner lê apps no ar como um desconhecido leria, e dos 31.056 apps que ele avaliou, três publicaram uma chave secreta do Supabase. Isso faz dela o achado mais raro que temos, e o mais grave.

O impulso ao lado é muito mais comum. Atrás de 8.435 desses apps encontramos um projeto Supabase. Em 3.680 deles a pergunta "um desconhecido consegue ler esta tabela" chegou a ter uma resposta aproveitável, e 2.096 responderam que sim: pelo menos uma tabela entregou linhas a uma requisição de fora sem nenhuma conta envolvida. Em 394 deles a tabela aberta tinha nome de tabela de pessoas.

Parte disso é proposital. Um cardápio publicado, uma página de anúncios e um changelog público vivem todos em tabelas que um desconhecido deve poder ler, e nosso scanner não sabe separá-las de uma tabela de clientes aberta por descuido. Ele relata o que respondeu e deixa o julgamento com você, que é a razão de ligar Row Level Security não ser o mesmo que estar protegido.

O que um desconhecido consegue ler, sobre os apps que responderam. A faixa tracejada são os apps que não responderam nada, o que não é a mesma coisa que um app sem nada aberto.

Se você prefere ver sua própria resposta a deduzi-la, nosso scan gratuito lê seu site no ar e diz o que ele alcança de fora. Leva uns 20 segundos e não precisa de conta: escanear seu app.

Como saber qual chave você colou

Leia o começo da string.

sb_publishable_… pertence ao seu app. sb_secret_… nunca. Não há nada a decodificar, porque para que serve a chave está escrito na frente dela.

Um valor longo que começa com eyJ é uma do par antigo, e é aí que as duas ficam difíceis de separar. A seção do meio de uma chave dessas é informação legível e não criptografia, e ela carrega um campo que resolve a pergunta:

{
  "iss": "supabase",
  "role": "anon",          ← o que importa
  "iat": 1750000000
}

anon é a publicável do par. service_role é a que você rotaciona hoje. A documentação do Supabase diz isso de forma mais seca do que diríamos: se um tutorial ou um assistente de IA manda copiar uma chave longa que começa com eyJ, ele foi escrito para as chaves antigas.

Os dois pares podem estar ativos ao mesmo tempo, e é nesse detalhe que as pessoas tropeçam. Criar uma chave publicável ou secreta deixa suas chaves anon e service_role exatamente onde estavam, e elas seguem funcionando até você desativá-las no painel, o que é um passo separado. O que mudou com as chaves novas cobre essa migração, e onde cada valor fica no seu painel é a página a abrir se você nunca esteve lá.

Se a chave secreta já está no seu app

Rotacione antes de editar qualquer coisa, e trabalhe na ordem que o Supabase dá.

Crie uma chave secreta nova no painel. Substitua a antiga em todos os lugares em que seu projeto a usa. Confirme que cada parte do app está na nova. Só então aposente a antiga, e esse último passo muda conforme o par que você tem na mão: apagar uma chave secreta não dá para desfazer, enquanto desativar um par anon e service_role antigo é reversível, então você pode religá-las se esqueceu algum cliente.

Rotacionar primeiro ou tapar o vazamento primeiro depende de já existir uma cópia pública. Rotacionar uma chave service_role do Supabase percorre as duas ordens e o que ler nos seus logs depois.

Para continuar assim quando o erro some

A correção vale até o próximo prompt ou a próxima mudança que mexer na sua configuração do Supabase. Basta um prompt para colocar uma chave de volta ou afrouxar uma regra de novo, e o seu app continua funcionando nos dois casos, então a mudança só aparece quando alguém olha.

O Reeve Monitor olha por você:

  • as nove verificações a cada hora, em até três apps
  • um e-mail no dia em que um novo scan der ao seu app uma nota mais baixa que o anterior
  • se o app está no ar, a cada 60 segundos
  • um relatório mensal do que ele viu

Uma chave que pode escrever cada linha também pode esvaziar cada tabela. O Reeve Care guarda uma cópia do seu banco Supabase onde nenhuma chave do seu app alcança.

  • uma cópia criptografada todo dia, guardada fora da sua conta do Supabase
  • cada cópia verificada antes de valer, com a contagem das linhas de cada tabela
  • uma restauração com um clique que salva o que existe agora antes de substituir qualquer coisa
  • os seus arquivos enviados também, assim que você conectar uma credencial de Storage
  • tudo o que o Monitor faz

O que fazer hoje

O que fazer

  • Leia a mensagem como uma recusa na porta. Suas linhas não foram tocadas e não há nada a recuperar.
  • Olhe na aba de rede do navegador a requisição que falhou e veja se existe um cabeçalho apikey ali. Essa única olhada diz se o problema é de chave ou de redirecionamento.
  • Crie seu cliente Supabase com a chave publicável e deixe a biblioteca enviar o cabeçalho. sb_publishable_… num projeto novo, a chave anon num antigo.
  • Se já existe uma chave secreta no seu app, rotacione primeiro. Apagar do código não fecha a porta, porque a versão antiga continua no seu histórico e em qualquer cópia em cache do site.
  • Devolva ao lugar qualquer regra que você afrouxou nessa caça, e depois confira de fora, não pelo preview do seu builder.

Comece pela única tabela de onde veio o erro, e depois leia as policies de cada tabela que você mexeu na mesma semana. A checklist de segurança de 10 minutos cobre o que mais costuma ficar aberto num app recém-lançado, e o guia de segurança do Supabase percorre o resto do que um desconhecido consegue alcançar.

Perguntas frequentes

O que significa "No API key found in request"?

Significa que sua requisição chegou ao Supabase sem o cabeçalho apikey, então a porta que o Supabase coloca na frente do seu projeto a recusou antes que o banco de dados entrasse na história. Nada foi lido, nada foi escrito e nada mudou nos seus dados. A resposta traz uma dica que diz o mesmo com outras palavras: no apikey request header or url param was found.

Qual chave do Supabase vai no cabeçalho apikey?

A sua chave publicável, que é sb_publishable_ em um projeto novo e a chave anon em um mais antigo. O Supabase entrega essa chave exatamente para isso e conta que ela fique visível dentro do seu app. A biblioteca cliente a coloca no cabeçalho em toda requisição, então se você cria o cliente com a chave publicável nunca escreve o cabeçalho na mão.

É seguro deixar minha chave anon no navegador?

Sim, e ela precisa estar lá para o seu app conseguir falar com o seu projeto. A chave anon só diz a que projeto uma requisição pertence; o que um visitante com ela de fato alcança é decidido pelas regras de Row Level Security das suas tabelas. Quais chaves de API são seguras no frontend percorre toda a família de chaves e mostra como lê-las.

Usei a chave service_role e funcionou. Isso é um problema?

Sim, e é o que vale a pena resolver hoje. Uma chave service_role carrega o atributo BYPASSRLS do Postgres, então suas policies nunca se aplicam a ela. Publicada dentro do seu app, ela fica em um arquivo que qualquer visitante pode baixar, e quem baixar consegue ler e escrever cada linha de cada tabela. Rotacione a chave primeiro e depois mova para um servidor aquilo que precisava dela.

Como sei qual chave eu colei?

Leia o começo da string. sb_publishable_ pertence ao seu app e sb_secret_ nunca. Um valor longo que começa com eyJ é uma do par antigo, e essas duas parecem idênticas, então é preciso olhar dentro: a seção do meio se decodifica em dados legíveis com um campo role, que diz anon ou service_role.

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

Não sabe como está o seu próprio app?

Faça um scan gratuito e receba uma nota clara de A a F em cerca de 20 segundos. Sem conta e sem cartão.

Verificar meu app grátis

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