name: supabase-self-hosted-login-loop-fix
description: Corrigir loop de login (flash) no Supabase Self-hosted — role supabase_admin no JWT causando SET ROLE fail no PostgREST
triggers:
- login flash loop
- permission denied to set role
- 42501 error
- login entra e sai piscando
Sintoma
Usuário faz login no Supabase Self-hosted (GoTrue/Auth) — token é gerado corretamente — mas o frontend mostra loop de login (entra e sai imediatamente). Logs do PostgREST mostram erro 42501: permission denied to set_config('role', 'supabase_admin', true).
Root Cause
Quando auth.users.role = 'supabase_admin' e is_super_admin = true, o GoTrue coloca role: supabase_admin no JWT. O PostgREST com VERIFY_JWT=false decoda esse JWT e tenta executar:
`sql
SELECT set_config('role', 'supabase_admin', true)
`
Isso falha para conexões não-superuser (o postgres do container não é superuser), gerando erro 42501. O PostgREST então retorna 403 em todas as requisições, e o frontend faz logout e redireciona para login → loop.
Passo a Passo
1. Diagnóstico
`bash
Verificar qual role o usuário tem no banco
docker exec deploy-vps-db-1 psql -U postgres -d postgres -c \
"SELECT id, email, role, is_super_admin FROM auth.users WHERE email = 'email@exemplo.com';"
Ver logs do PostgREST (erro 42501)
docker logs deploy-vps-rest-1 --since 5m | grep -i "error\|denied\|42501"
Ver logs do Auth (token sendo gerado)
docker logs deploy-vps-auth-1 --since 5m | grep -i "login\|token"
`
2. Verificar JWT gerado
`bash
Fazer login e extrair token
RESPONSE=$(curl -s -X POST https://dominio.com.br/auth/v1/token?grant_type=password \
-H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"email@exemplo.com","password":"senha"}')
TOKEN=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
Decodificar JWT (base64 decode do payload)
echo $TOKEN | cut -d. -f2 | python3 -c "import sys,json,base64; print(json.dumps(json.loads(base64.b64decode(s + '=='))))"
`
Procurar no payload: "role": "supabase_admin" — indica o problema.
3. Correção
Mudar role do usuário de supabase_admin para authenticated:
`sql
-- NÃO usar supabase_admin para usuários normais
UPDATE auth.users
SET role = 'authenticated', is_super_admin = false
WHERE email = 'email@exemplo.com';
`
Importante: authenticated é o role correto para usuários normais logados. O supabase_admin deve ser usado apenas para contas de admin do sistema (superuser do banco).
4. Verificar se funcionou
`bash
Login novamente e verificar token
RESPONSE=$(curl -s -X POST https://dominio.com.br/auth/v1/token?grant_type=password \
-H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"email@exemplo.com","password":"senha"}')
`
O token deve conter "role":"authenticated" (não supabase_admin).
Evitar o Problema no Futuro
Evitar o Problema no Futuro
- - Nunca criar usuários diretamente no banco com role
supabase_adminpara uso normal - - Usuários criados via
/auth/v1/signuprecebemauthenticatedpor padrão — correto - - Se precisar de admin, usar
/auth/v1/admin(GoTrue admin API) ou mudar apenasis_super_admin = truesem mudar o role parasupabase_admin
Outras Causas do Login Loop (Descobertas em Produção)
Além do role: supabase_admin, o login loop pode ocorrer por:
Causa 2: Tabelas sem RLS policy para authenticated
O frontend usa authenticated JWT. Se auth.users, public.profiles, etc. não tiverem policies que permitem SELECT para role authenticated, o frontend tenta ler dados e recebe 403/401, causando crash no JS e reload.
Verificar:
`sql
-- auth.users precisa de policy para SELECT pelo role authenticated
ALTER TABLE auth.users ENABLE ROW LEVEL SECURITY;
CREATE POLICY auth_users_authenticated_select ON auth.users
FOR SELECT TO authenticated USING (true);
-- public.profiles
CREATE POLICY public_profiles_select ON public.profiles
FOR SELECT TO authenticated USING (true);
`
Causa 3: auth.instances vazio
Se a tabela auth.instances estiver vazia, GoTrue pode não inicializar corretamente.
Verificar e corrigir:
`sql
SELECT * FROM auth.instances;
-- Se vazio, inserir instância default:
INSERT INTO auth.instances (id, uuid, name, postgresql_conf, jwt_secret, jwt_exp)
VALUES ('00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000000',
'Default Instance',
'{}',
'JWT_SECRET_DO_GOTRUE',
3600);
`
Causa 4: auth.users sem app_metadata.provider
O /auth/v1/user endpoint (chamado pelo Supabase JS após login) precisa retornar app_metadata.provider. Users criados via /signup têm isso correto; users inseridos direto no DB não têm.
Verificar:
`json
{
"app_metadata": { "provider": "email", "providers": ["email"] },
"user_metadata": {}
}
`
Se provider estiver ausente ou vazio, o Supabase JS client pode interpretar como erro e disparar logout.
Diagnóstico: o loop exato no browser
O login funciona (200 + JWT), mas o navegador chama /logout (204) em seguida.
Possíveis motivos no browser:
1. signInWithPassword() callback recebe erro (campo faltante no user object)
2. getSession() retorna null após login
3. onAuthStateChange dispara evento SIGNED_OUT
4. Erro no console do navegador (extensões, CSP local)
Para diagnosticar: pedir ao usuário para abrir DevTools (F12) → aba Console → fazer login → observar mensagens de erro vermelhas.