📄 SKILL.md

← Vault

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

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.