Pular para o conteúdo
🔧 AJUDA

Troubleshooting.

Erros que aparecem com frequência, com a causa e o conserto, por área. Cada código de erro aqui é o que a API devolve de verdade. Se o seu não estiver na lista, abra um chamado.

💡

Antes de tudo: abra o DevTools → Network e olhe a requisição que falhou. O status, o corpo da resposta (quase sempre {"error":{"code":"…"}}) e os headers resolvem a maior parte dos casos. Guarde um print disso se for abrir chamado.

Conexão e rede

"Failed to fetch" / "Network error"

Causa: a requisição nem terminou. URL errada, extensão de bloqueio, rede fora do ar, ou uma resposta de erro sem os headers de CORS.

  1. A URL do SDK é a do auth: https://auth.superdb.com.br, sem barra no fim. O endereço dos dados (api.superdb.com.br) o SDK deduz sozinho.
  2. Teste de fora do app: curl -i https://auth.superdb.com.br/.well-known/jwks.json responde 200 quando o serviço está no ar.
  3. Desligue extensões de bloqueio e tente numa aba anônima.

A requisição demora ou dá timeout

Causa: quase sempre uma consulta sem índice, que percorre a tabela inteira.

Solução: rode a consulta com EXPLAIN ANALYZE no SQL Editor e crie o índice que falta. A tela Saúde do projeto aponta chave estrangeira sem índice e tabela sem RLS.

429 em rajadas

Causa: muitas requisições do mesmo IP em pouco tempo. Acontece com app renderizado no servidor, em que todos os usuários saem pelo IP do servidor.

Solução: use o @superdb/client 0.2.2 ou mais novo: ele reenvia sozinho quando o Retry-After é curto. Um 429 com espera longa é proteção contra força bruta (login, código), e o SDK devolve para você decidir.

Login e sessão

"project not found" / 404 na primeira chamada

Causa: falta o header X-SuperDB-Project. A chave anon não identifica o projeto para o auth: quem faz isso é esse header (ou o Host, se você configurou domínio próprio).

Solução: mande X-SuperDB-Project: <slug> em toda chamada a auth.superdb.com.br:

curl
curl -X POST https://auth.superdb.com.br/auth/v1/signin \
  -H "Content-Type: application/json" \
  -H "X-SuperDB-Project: meu-projeto" \
  -d '{"email":"...","password":"..."}'

Com o SDK, o header sai sozinho do project passado no createClient. A API de dados não precisa dele: lá, o projeto sai da própria credencial.

404 num endpoint de auth que "existe"

Causa: caminho do Supabase. O SuperDB não tem /auth/v1/token?grant_type=password nem /auth/v1/logout.

Solução: login é POST /auth/v1/signin, sair é POST /auth/v1/signout, renovar é POST /auth/v1/token/refresh e o código por e-mail ou WhatsApp é POST /auth/v1/signin/otp. A lista completa está em API REST.

401 depois de uma hora de uso ("JWT expired", PGRST301)

Causa: a sessão vale 1 hora e venceu sem ser renovada. O SDK renova sozinho desde a versão 0.3, um minuto antes de vencer; as versões anteriores não renovam, e quem chama a API por HTTP direto (ou cria o client com autoRefreshToken: false) renova por conta própria.

Solução: atualize o @superdb/client para a 0.3 ou mais nova. Sem o SDK, chame POST /auth/v1/token/refresh antes de vencer; com a sessão em cookies (Next.js), quem renova é o proxy.ts dos templates oficiais. Se o refresh também falhar, mande a pessoa entrar de novo.

401 na API de dados logo depois do login

Causa: o access_token foi mandado para api.superdb.com.br. Ele é um token PASETO, da API de auth; a de dados só aceita o data_plane_token, que chega no mesmo login.

Solução: com o SDK, isso não acontece. Em HTTP direto, mande Authorization: Bearer <data_plane_token>. Veja qual token vai em cada lugar.

refresh_token_reuse_detected e a pessoa cai do app

Causa: o mesmo refresh token foi usado duas vezes, quase sempre por duas abas renovando ao mesmo tempo. O SuperDB trata isso como roubo e derruba a família inteira de sessões.

Solução: renove uma vez por vez. O sessao.ts dos templates usa Web Locks para isso.

invalid_credentials (401) no login

Causa: e-mail ou senha errados. A resposta é a mesma para e-mail que não existe, de propósito. Conta criada por código ou pelo Google não tem senha e também cai aqui.

Solução: ofereça entrar com código no e-mail: funciona para qualquer conta com aquele e-mail. Depois de entrar, a pessoa pode criar uma senha.

A senha parou de funcionar depois de entrar por código ou pelo Google

Causa: a conta foi criada com senha e nunca confirmou nem e-mail nem telefone (o cadastro por senha não confirma). Quem prova ser dono do e-mail, pelo código, pelo link, pelo Google ou redefinindo a senha, passa a ser o dono da conta. Tudo o que existia antes dessa prova cai: a senha, as sessões abertas, os fatores de 2 etapas e os outros logins sociais. É o que impede alguém de cadastrar o e-mail de outra pessoa e esperar ela chegar. Acontece uma vez por conta, e a auditoria do projeto registra o evento account.claimed.

Solução: a pessoa continua entrando por código ou pelo Google. Para voltar a ter senha, ela cria uma nova depois de entrar, ou usa o "esqueci a senha".

account_locked (423) ou 429 no login

Causa: muitas senhas erradas seguidas. O bloqueio é temporário e protege contra força bruta.

Solução: mostre um aviso para esperar alguns minutos e ofereça o login por código.

user_already_exists (409) no cadastro

Causa: já há conta com aquele e-mail ou telefone.

Solução: leve para o login, com a opção de entrar por código.

"Esqueci a senha"

Peça o e-mail com a página do seu app que recebe a nova senha, e cadastre essa página, exatamente como ela é, em Autenticação → URLs de redirecionamento. Página não cadastrada não recebe o link: a chamada é recusada com redirect_to_nao_cadastrado e nenhum e-mail é enviado.

Hospeda na Vercel, na Netlify ou no Cloudflare Pages? O endereço de preview muda a cada publicação: cadastre o padrão do seu time, como https://*-seutime.vercel.app. O * só vale no começo do endereço, com o resto fixo; *.vercel.app é recusado no cadastro.

Projeto sem nenhuma URL cadastrada recusa o pedido com projeto_sem_url_de_retorno. Aí o dono do projeto também fica sabendo: recebe um e-mail com o passo a passo (no máximo um por dia), e o alerta aparece em Advisors, com o link para a tela que resolve.

pedir o e-mail
curl -X POST https://auth.superdb.com.br/auth/v1/password/forgot \
  -H "X-SuperDB-Project: meu-projeto" \
  -H "Content-Type: application/json" \
  -d '{"email":"ana@exemplo.com.br","redirect_to":"https://meuapp.com.br/nova-senha"}'

O link do e-mail abre https://meuapp.com.br/nova-senha#token=…. A sua página lê o token do fragmento (ele não chega ao seu servidor) e manda a senha nova:

trocar a senha
curl -X POST https://auth.superdb.com.br/auth/v1/password/reset \
  -H "X-SuperDB-Project: meu-projeto" \
  -H "Content-Type: application/json" \
  -d '{"token":"<token do fragmento>","new_password":"uma-senha-nova"}'

O token vale 1 hora e só uma vez. O pedido responde {"ok":true} mesmo para e-mail sem conta, para não revelar quem é cliente. O link mágico segue a mesma regra de URLs cadastradas.

Se o projeto tem a conferência de senha vazada ligada, a senha nova que aparece em vazamento conhecido volta password_breached (400), como no cadastro. O token não é gasto: a pessoa escolhe outra senha com o mesmo link.

Quem entrou por código ou pelo Google quer ter senha

Causa: conta criada por código, link mágico ou login social nasce sem senha.

Solução: com a pessoa logada, mande a senha nova com o access_token da sessão. Conta sem senha cria a primeira sem pedir a atual:

criar a senha
curl -X PUT https://auth.superdb.com.br/auth/v1/user/password \
  -H "X-SuperDB-Project: meu-projeto" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{"new_password":"uma-senha-nova"}'

A resposta traz "criada": true. Conta que já tem senha troca pela mesma rota, mandando também a atual em current_password. Sem ela, a resposta é current_password_required (400). Na troca, as outras sessões da pessoa caem. Criar e trocar mandam o e-mail de "senha alterada". Durante o "entrar como" do painel, criar senha é recusado: a senha é da pessoa. Com a conferência de senha vazada ligada no projeto, criar ou trocar para uma senha que aparece em vazamento conhecido também volta password_breached (400).

O código não chegou

Causa: spam, e-mail digitado errado, ou o limite de pedidos (10 por hora, por IP, no projeto).

Solução: não existe "reenviar": chame signInWithOtp de novo, e o código novo vale 10 minutos. Mostre o endereço na tela do código, para a pessoa conferir. Para melhorar a entrega, verifique o seu domínio em Emails → Remetente dos emails.

whatsapp_not_in_plan (403) ao pedir código por WhatsApp

Causa: o código por WhatsApp faz parte dos planos Pro e Escala, e o projeto está em outro plano. Nenhum código foi gerado nem enviado.

Solução: mude o plano no painel, no menu do projeto em Billing (compare os planos em Preços), ou peça o código por e-mail (channel: 'email'), que vale em todos os planos.

whatsapp_send_failed (503) ao pedir código por WhatsApp

Causa: o código não saiu. Três motivos, e a mensagem do erro diz qual: o número de destino não tem WhatsApp; o número que envia foi desconectado; ou o número compartilhado do SuperDB chegou ao teto (60 mensagens por hora e 300 por dia, somando todos os projetos que não pareiam o próprio).

Solução: caia para o e-mail no app (channel: 'email'), como na receita. Se o seu projeto manda muito código por WhatsApp, pareie o seu número em Autenticação → WhatsApp do projeto: o teto do número compartilhado não vale para ele.

Row Level Security

"new row violates row-level security policy"

Causa: o WITH CHECK da policy recusou o INSERT ou UPDATE. Quase sempre o user_id da linha não é o auth.uid().

Solução: deixe o banco preencher o dono e não mande user_id do app:

SQL Editor
alter table tasks alter column user_id set default auth.uid();

"permission denied for table X"

Causa: falta o GRANT para a role (authenticated ou anon). Não é o RLS: é a permissão do Postgres, que vem antes.

SQL Editor
grant select, insert, update, delete on tasks to authenticated;
-- leitura pública, só se a tabela for mesmo pública:
grant select on posts to anon;

A consulta volta vazia, mas a tabela tem dados

Causa: o RLS. Toda tabela nova nasce fechada (RLS ligado, nenhuma policy), e consulta bloqueada volta lista vazia, não erro.

Solução: crie a policy (veja Row Level Security). Atenção: o SQL Editor roda como dono do projeto, que passa por cima do RLS. Lá a tabela sempre mostra tudo; para ver o que o usuário vê, consulte pelo app logado.

A tabela devolve tudo para todo mundo

Causa: uma policy com using (true), ou uma tabela antiga, criada antes de o RLS vir ligado por padrão (17/08/2026).

Solução: a tela Saúde lista tabelas sem RLS. Ligue com alter table x enable row level security; e escreva as policies.

Aviso: "a RLS de X está desligada, e a tabela ficou exposta pela API"

Causa: um alter table x disable row level security numa tabela que a API alcança. No SuperDB toda tabela nova já dá leitura ao anon e escrita ao authenticated: a RLS é a única porta. Desligada, qualquer pessoa com a chave pública do seu app lê a tabela inteira, e as policies que você criou deixam de valer, porque o Postgres não as consulta. O aviso vem na resposta do próprio comando: no SQL Editor, no campo avisos do /db/exec, no MCP e, numa conexão direta ao Postgres, como WARNING.

Solução: religue a RLS e libere só o necessário. Desligar a RLS quase nunca conserta "o insert deu erro": o que falta é uma policy de INSERT.

SQL Editor
alter table fotos enable row level security;
-- leitura pública, só se a tabela for mesmo pública:
create policy "leitura" on fotos for select to anon, authenticated using (true);
-- gravação só do dono:
create policy "dono grava" on fotos for insert to authenticated
  with check (user_id = auth.uid());

Meu grant ... to public virou anon e authenticated

Causa: é de propósito. PUBLIC não é "o público do seu app": no PostgreSQL ele vale para todo papel do servidor de banco, não só para os do seu projeto. O banco converte o grant na hora para anon e authenticated, os papéis que a API de dados usa, e avisa na resposta.

Solução: nada muda para o seu app: o acesso pela API é o mesmo de antes. Nas suas migrations, escreva direto grant ... to anon, authenticated e controle as linhas com RLS.

API REST

416 "Range not satisfiable"

Causa: a paginação pediu uma faixa além do fim (.range(1000, 1999) numa tabela com 50 linhas).

Solução: pare de paginar quando a página vier menor que o tamanho pedido.

406 com .single()

Causa: .single() exige exatamente uma linha, e vieram zero ou duas.

Solução: use .maybeSingle() quando zero linhas for um resultado normal.

"PGRST204" / "PGRST205": coluna ou tabela não encontrada

Causa: nome errado, ou a tabela acabou de ser criada. O cache de schema recarrega sozinho depois de uma mudança de estrutura no projeto, em alguns segundos.

Solução: confira o nome (maiúsculas contam) e tente de novo depois de alguns segundos.

Contar linhas

Com o SDK (0.3 em diante), select('*', { count: 'exact', head: true }) devolve o total em count, sem trazer as linhas. Pela API REST, mande Prefer: count=exact e leia o total no Content-Range. Em tabela grande, estimated no lugar de exact é instantâneo e aproximado. Exemplo em PostgREST filtros.

Embed ?select=*,autor:usuarios(*) não funciona

Causa: não há chave estrangeira entre as duas tabelas, e o PostgREST precisa dela para montar o join.

SQL Editor
alter table posts add constraint posts_autor_fk foreign key (autor_id) references usuarios (id);

SQL e extensões

net.http_post ou um Database Webhook do Supabase não funciona

Causa: o pg_net não está disponível nos bancos dos projetos, assim como o pg_cron. Sem ele, net.http_post não chama URL nenhuma, e os Database Webhooks do Supabase, que são um gatilho em cima dele, não migram.

Solução: tire a chamada HTTP de dentro do banco. A sua aplicação chama a URL logo depois de gravar; ou um gatilho grava o evento numa tabela sua, e um worker (o seu backend ou um Cloudflare Worker) lê essa tabela e chama a URL. Tarefa agendada segue a mesma ideia: o agendador fica fora do banco e chama uma função via RPC.

Storage

⚠️

O Storage isola projetos, não usuários. Qualquer usuário logado no seu projeto lê, envia e apaga arquivos em todos os buckets do projeto. A chave anon só lê bucket público. Não guarde no Storage um arquivo que um usuário não possa ver de outro.

Upload volta 403

Causa: ninguém logado. Sem sessão, o SDK troca a chave anon por um token de storage que só lê bucket público.

Solução: faça o upload com o usuário logado, ou do seu servidor com a service key.

402 em /st/v1/token

Causa: a cota de Storage do plano acabou, e o SuperDB para de emitir token de storage.

Solução: apague arquivos que sobraram ou mude de plano, em Billing.

"Bucket not found"

Causa: o bucket não existe ou o nome está diferente (maiúsculas contam: Avatares e avatares são dois buckets).

Solução: confira o nome na tela Storage do projeto.

Realtime

Conectou, mas os eventos não chegam

  1. A tabela está ligada na tela Realtime do projeto? Só entra tabela com RLS ligada: sem ela, o painel recusa com "Ligue a RLS de … antes de ativar o Realtime". Ligue a RLS, escreva as policies e ative de novo.
  2. O schema do canal é proj_ mais o slug do projeto?
  3. O token foi pedido com a sessão do usuário? Com a anon key, o realtime entrega só o que um visitante sem login leria. Numa tabela cujo RLS libera só quem está logado, isso é nada. Peça o token com db.auth.getDataPlaneToken().
  4. A regra de leitura da tabela consulta outra tabela? Ela precisa usar uma função security definer; senão não vale no realtime. O painel avisa ao ativar a tabela.
  5. O token de realtime vale 1 hora. Troque por um novo com rt.setAuth(token) antes de vencer.
  6. O db.channel() do SDK ainda não funciona. A conexão é pelo @supabase/realtime-js com um token de /rt/v1/token; veja SDK JS → Realtime.
🔒

O realtime segue o RLS. Cada inscrito recebe só as linhas que a credencial dele leria pela API. Se a tela precisa de um dado que o usuário não lê pela API, ela também não o recebe pelo realtime.

O /rt/v1/token responde 401 ou 403

  • 401 API key revogada ou inválida: as chaves do projeto foram regeneradas no painel. Use a anon key nova, da aba API Keys.
  • 401 invalid token com a sessão: ela venceu (vale 1 hora; o SDK renova sozinho desde a 0.3, as versões anteriores não). Chame await db.auth.refreshSession() e peça de novo.
  • 403 forbidden: not a user of …: o usuário da sessão foi banido ou apagado do projeto.
  • 403 project is not active: o projeto está suspenso. O motivo aparece no painel.

E-mails

O e-mail chega no spam

Causa: o projeto envia pelo remetente padrão do SuperDB.

Solução: em Emails → Remetente dos emails, verifique o seu domínio. Não há SMTP para configurar.

Uma variável do template não é substituída

Causa: nome de variável que o template não recebe. A sintaxe é {{nome}}, sem ponto.

Solução: no Código MFA (que também é o código de login), use {{code}} e {{expires_in_minutes}}. Nos templates de link, o endereço é {{url}}; o editor ainda lista {{link}} e {{name}}, que saem em branco.

CORS

"blocked by CORS policy"

As duas APIs respondem Access-Control-Allow-Origin: *: não existe lista de origens para configurar, e localhost funciona igual a produção. Quando o navegador fala em CORS, a causa é outra:

  • Header fora da lista, no auth. auth.superdb.com.br aceita só Authorization, apikey, Content-Type, X-SuperDB-Project, Accept-Profile, Content-Profile e X-Client-Info. Um header seu a mais derruba a requisição antes de ela sair.
  • Uma resposta de erro sem CORS. Um 502 ou 413 da borda chega sem os headers, e o navegador mostra como erro de CORS. Veja o status real na aba Network.
  • URL errada, com /rest/v1 no caminho (isso é do Supabase) ou com http://.

Login social

redirect_uri_mismatch na tela do Google

Causa: com credenciais próprias, o endereço de callback do SuperDB não está no console do provedor.

Solução: cadastre exatamente https://auth.superdb.com.br/auth/v1/callback/<provider> (google, github ou apple).

oauth_not_enabled

Causa: o provedor não está ligado no projeto.

Solução: Autenticação → Login social (OAuth). O modo gerenciado não pede chave nenhuma.

oauth_state_unbound depois de escolher a conta

Causa: o redirectTo não é igual, caractere por caractere, a uma URL cadastrada no projeto.

Solução: cadastre em Autenticação → URLs de redirecionamento. localhost e 127.0.0.1 contam como endereços diferentes. O passo a passo está em Signup com Google.

no_pkce_verifier na página de volta

Causa: a troca do code rodou num navegador diferente do que começou o login, ou foi chamada duas vezes.

Solução: refaça o login pelo botão, e troque o code uma vez só.

MFA

mfa_invalid_code com o código certo

Causa: o relógio do celular está adiantado ou atrasado. O código TOTP depende da hora.

Solução: ligue a hora automática no aparelho e use o próximo código.

A pessoa perdeu o celular do autenticador

O login ainda não pede o segundo fator, então ela entra com a senha, remove o fator antigo e cadastra o novo celular. O passo a passo, com os códigos de reserva, está em MFA TOTP.

ℹ️

Não achou o seu erro? Abra um chamado no painel com: (1) o erro exato, (2) o que você estava tentando fazer e (3) um print da aba Network.

Essa página ajudou?