Antes de mais nada: abre o DevTools → Network e vê o request que falhou. 90% dos bugs ficam óbvios vendo o status code, o response body e os headers. Tire um print disso antes de pedir ajuda.
Conexão & rede
"Failed to fetch" / "Network error"
Causa: client não conseguiu nem completar o TCP handshake. CORS bloqueado, URL errada, ou serviço offline.
Solução:
- Cheque
NEXT_PUBLIC_SUPERDB_URL— deve serhttps://api.superdb.com.br(sem trailing slash, sem path) - Abra o Network tab; se o request é blocked by CORS, veja seção CORS
- Teste o endpoint cru:
curl https://api.superdb.com.br/ -H "apikey: SUA_ANON_KEY"— sem prefixo/rest/v1/(PostgREST do SuperDB serve na raiz). Se isso falha, o problema é seu network (proxy, firewall) - Status do SuperDB: status.superdb.com.br
"ECONNREFUSED" em self-host
Causa: container do PostgREST não está respondendo. Pode estar caindo no boot por falta de schema/role.
Solução: docker logs superdb-rest. Erros típicos: role "authenticated" does not exist (rode db reset ou refaça init); connection refused a Postgres (DB ainda subindo — espere 30s).
"SSL error: certificate verify failed"
Causa: client com root CA desatualizado, ou DNS errado apontando pra IP sem cert.
Solução: atualize Node/curl pra versão recente. Se for self-host com cert auto-assinado, configure o client pra confiar ou use Let's Encrypt (recomendado).
Request timeout em produção
Causa: query lenta (full scan), connection pool cheio, ou rede congestionada.
Solução: rode EXPLAIN ANALYZE da query no Studio. Sem índice = full scan. Use CREATE INDEX e mede de novo.
Autenticação
"JWT expired"
Causa: access token caducou (1h) e o SDK não fez refresh — ou o user passou tempo offline.
Solução: await db.auth.refreshSession(). Se falhar, force re-login: await db.auth.signOut() → redirect pra /login. Verifique também se está usando o SDK oficial — implementações próprias podem esquecer de fazer refresh.
"Invalid JWT" / "signature is invalid"
Causa: JWT secret do projeto foi rotacionado, mas o token foi emitido com o antigo. Ou token foi adulterado.
Solução: force re-login. Em produção, rotacionar secret invalida todos os JWTs ativos — comunique users que vão precisar logar de novo.
"Invalid login credentials"
Causa: senha errada, email não confirmado, ou usuário banned.
Solução: ofereça "esqueci senha". Se email não foi confirmado: peça pra checar inbox (incluindo spam) e reenvie com db.auth.resend({ type: 'signup', email }).
"Email not confirmed"
Causa: setting Confirm email está ativo (default em Pro+) e o user nunca clicou no link.
Solução: reenvie a confirmação via db.auth.resend(). Ou, em dev, desligue Confirm email no Dashboard → Auth → Settings.
"User already registered"
Causa: tentando signup com email que já existe.
Solução: redirecione pro login. Se quer permitir múltiplos providers no mesmo email, habilite Link identical email em Auth Settings (cuidado com phishing).
Row Level Security
"new row violates row-level security policy"
Causa: INSERT/UPDATE bloqueado pelo WITH CHECK da policy. Provavelmente user_id da row não bate com auth.uid().
Solução: ou seta user_id = auth.uid() antes de inserir, ou use DEFAULT auth.uid() na coluna:
ALTER TABLE tasks ALTER COLUMN user_id SET DEFAULT auth.uid();
-- Aí o client nem precisa passar user_id
"permission denied for table X"
Causa: a role do JWT (authenticated ou anon) não tem GRANT na tabela. RLS não é o problema — é o grant de Postgres.
Solução:
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE tasks TO authenticated;
-- Pra leitura pública, adicione anon
GRANT SELECT ON TABLE posts TO anon;
SELECT retorna vazio mas tem dados
Causa #1: RLS está bloqueando — sua policy não inclui o user atual.
Causa #2: Está usando anon key onde precisava de JWT de user. Anon não tem auth.uid().
Solução: no Studio, rode SET ROLE authenticated; SET request.jwt.claims = '{"sub":"USER_ID"}'::jsonb; SELECT * FROM tasks; pra simular o que o user vê. Se aparece tudo na role postgres mas vazio em authenticated, é RLS.
RLS configurada mas retorna tudo pra todo mundo
Causa: esqueceu ALTER TABLE x ENABLE ROW LEVEL SECURITY. Sem isso, policies são ignoradas.
Solução: rode SELECT relname, relrowsecurity FROM pg_class WHERE relname = 'sua_tabela'; — relrowsecurity tem que ser t.
PostgREST / API REST
"Range not satisfiable" / 416
Causa: paginação pediu Range: 1000-1999 mas só existem 50 rows.
Solução: obtenha o Content-Range: 0-49/50 primeiro, calcule a página final. Ou use offset/limit do SDK que cuida disso.
"PGRST204: column X not found"
Causa: coluna não existe ou cache do schema está stale.
Solução: verifica spelling. Se a coluna existe mesmo, force reload do schema cache: NOTIFY pgrst, 'reload schema'; no SQL Editor.
"PGRST301: JWT expired"
Veja "JWT expired" acima.
Queries com count=exact ficam lentas
Causa: Postgres faz SELECT count(*) que escaneia a tabela inteira.
Solução: use count=estimated em tabelas grandes — usa estatísticas do pg_class, é instantâneo.
const { data, count } = await db
.from('events')
.select('*', { count: 'estimated' }) // ← em vez de 'exact'
.limit(20)
Embed ?select=*,author:users(*) não funciona
Causa: não há FK entre posts.author_id e users.id. PostgREST precisa da FK pra inferir o join.
Solução: ALTER TABLE posts ADD CONSTRAINT posts_author_fk FOREIGN KEY (author_id) REFERENCES users(id);
Storage
"Bucket not found"
Causa: bucket não foi criado ou nome está errado (case-sensitive).
Solução: Studio → Storage → "New bucket". Bucket name é case-sensitive: Avatars ≠ avatars.
Upload retorna 403
Causa: policy do bucket nega INSERT, ou bucket é público mas client tá enviando JWT vazio.
Solução: cheque policies em Storage → Bucket → Policies. Pra upload por usuário logado, a policy padrão é INSERT TO authenticated WITH CHECK (bucket_id = 'avatars').
Signed URL retorna 403 mesmo recém-criada
Causa: servidor com clock dessincronizado, ou TTL muito curto.
Solução: garante NTP no server (ntpdate). Aumente TTL pra 3600s pra teste.
"Payload too large" / 413
Causa: arquivo passa do limite do bucket (default 50MB no plano Free, 500MB Pro).
Solução: ajuste o limite em Bucket settings, ou use upload resumable (TUS) que aceita arquivos gigantes em chunks.
Realtime
Subscription criada mas eventos não chegam
Causa #1: tabela não está em publication supabase_realtime.
Causa #2: RLS está bloqueando — você não recebe eventos de rows que não consegue ler.
Solução: ALTER PUBLICATION supabase_realtime ADD TABLE tasks; + garante que a policy SELECT cobre o user.
WebSocket desconecta a cada 30s
Causa: proxy/load balancer com idle timeout curto, ou heartbeat desabilitado.
Solução: SDK manda heartbeat a cada 25s por default. Se está atrás de proxy, garante que proxy_read_timeout ≥ 60s no nginx.
"too many subscriptions"
Causa: excedeu o limite do plano (Free: 200 conn simultâneas).
Solução: consolide subscriptions (1 canal com filtros vs N canais), ou upgrade plano.
SMTP / Emails
"Email rate limit exceeded"
Causa: SMTP default tem limite de ~30 emails/dia (anti-abuse). Em produção, isso é insuficiente.
Solução: configure SMTP próprio (Resend, SendGrid, Postmark) em Dashboard → Auth → Email Settings. Free tier desses providers cobre milhares de emails.
Email chegando no spam
Causa: usando o SMTP default do SuperDB (compartilhado, sem SPF/DKIM no seu domínio).
Solução: use SMTP próprio com domínio verificado (SPF + DKIM + DMARC). Resend tem setup guiado em 10 min.
Variável {{ .ConfirmationURL }} não substitui
Causa: erro de sintaxe no template — case-sensitive e precisa do ponto.
Solução: use exatamente {{ .ConfirmationURL }} (Go template syntax). Veja todas as variáveis disponíveis em Auth → Email Templates.
CORS
"blocked by CORS policy"
Causa: origem do seu app (https://meu-app.com.br) não está na allowlist do projeto.
Solução: Dashboard → Settings → API → Allowed origins. Adicione com o protocolo: https://meu-app.com.br (não meu-app.com.br).
Funciona em prod mas não em localhost:3000
Causa: http://localhost:3000 não está na allowlist de dev.
Solução: adicione http://localhost:3000 (HTTP, não HTTPS) em Allowed origins. Pra Vite default: http://localhost:5173.
"CORS policy: credentials flag must be true"
Causa: SDK tentando enviar cookie mas server não tem Access-Control-Allow-Credentials: true.
Solução: esse header é setado automaticamente pelo SuperDB pra origens da allowlist. Se aparece, é porque a origem não está allowed — volte e adicione exata.
OAuth / providers
"Invalid redirect_uri"
Causa: URL de callback que o SuperDB envia ao Google/GitHub não está nas Authorized redirect URIs do provider.
Solução: no console do provider (ex: Google Cloud Console), adicione exatamente https://api.superdb.com.br/auth/v1/callback — byte a byte, sem trailing slash.
"OAuth provider not configured"
Causa: você chamou signInWithOAuth({ provider: 'google' }) mas não setou Client ID/Secret no Dashboard.
Solução: Dashboard → Auth → Providers → Google → ative + cole credenciais.
OAuth redireciona pra localhost em produção
Causa: setting Site URL aponta pra http://localhost:3000.
Solução: Dashboard → Auth → URL Configuration → Site URL = seu domínio de prod. Adicione localhost em Redirect URLs (allowlist) pra dev continuar funcionando.
MFA
"MFA challenge expired"
Causa: challenge tem TTL de 5 minutos. Usuário demorou demais entre challenge() e verify().
Solução: reinicie o fluxo — chame db.auth.mfa.challenge() de novo.
"Invalid TOTP code" mesmo com código certo
Causa #1: clock skew — celular e servidor com horário diferente em mais de 30s.
Causa #2: usou código de outro factor (você tem 2 enrolled).
Solução: sincronize horário do celular (auto). Liste factors enrolled: db.auth.mfa.listFactors() e mire no certo.
Não consigo desativar MFA — quero entrar mas perdi o app
Causa: esperado — desativar MFA requer prova de posse (recovery codes ou MFA atual).
Solução: use recovery codes (gerados no enroll). Se perdeu também: admin com service-role pode chamar db.auth.admin.deleteFactor(). Comunicação pelo email cadastrado é boa prática antes de fazer isso.
Não achou seu erro? Manda email pra contato@superdb.com.br com: (1) o erro exato, (2) o que você tava tentando fazer, (3) print do Network tab. A gente responde no mesmo dia útil e adiciona aqui.