name: supabase-self-hosted-edge-function-401-debug
description: Debug 401 Unauthorized em Edge Functions no Supabase self-hosted (Docker/VPS) — JWT_SECRET mismatch, architecture oculta, técnicas de diagnóstico.
category: devops
tags: [supabase, self-hosted, edge-function, jwt, 401, docker, vps]
Supabase Self-Hosted Edge Function 401 Debug
Debug 401 Unauthorized em Edge Functions no Supabase self-hosted (Docker/VPS).
Sintomas
- - Login funciona, token é retornado
- -
/functions/v1/mainretorna 200 com o mesmo token - -
/functions/v1/manage-usersretorna 401 Unauthorized - - Token é matematicamente válido (HMAC check passa em Python)
- -
main/index.ts= router que verifica JWT e faz dispatch interno - -
manage-users/index.ts= NÃO usado para HTTP, é código de biblioteca compartilhado - - O runtime é configurado com flags como
--main-service /home/deno/functions/maine--allow-net - - JWT_SECRET usado pelo main/index.ts para
verifyTokenInline() - -
sedfalha com secrets base64 contendo+— usar Python local - -
manage-users/index.tsé irrelevante para o endpoint HTTP — sómain/index.tsimporta - -
printenv | wc -cadiciona 1 por causa do newline — usarprintf "%s" - - Containers não têm Python — sempre copiar arquivos para local, editar, copiar de volta
- - Projeto Supabase Cloud vs self-hosted são DIFERENTES — mesmo projeto pode compartilhar nome mas configurações diferentes
- - VPS: 31.97.243.106
- - Compose project:
deploy-vps - - Containers:
deploy-vps-auth-1,deploy-vps-functions-1,deploy-vps-functions-proxy - - Auth usa
GOTRUE_JWT_SECRET, functions usaJWT_SECRET - - SSH:
sshpass -p 'senha' ssh root@31.97.243.106
Causa comum
JWT_SECRET mismatch entre o container auth (GoTrue) e o container functions (Deno runtime).
O .env em /docker/deploy-vps/.env pode ter o secret com trailing \n (56 bytes em vez de 44 bytes), causando mismatch na assinatura.
Diagnóstico passo a passo
1. Medir string length real (sem newline)
`bash
No auth container
docker exec deploy-vps-auth-1 sh -c 'printf "%s" "$GOTRUE_JWT_SECRET" | wc -c'
No functions container
docker exec deploy-vps-functions-1 sh -c 'printf "%s" "$JWT_SECRET" | wc -c'
`
printf "%s" não adiciona newline, diferente de echo ou printenv.
2. Verificar architecture — Edge Functions usam main/index.ts
Edge Functions NOVO estilo (runtime Deno) são servidas pelo arquivo main/index.ts no container, NÃO pelo arquivo da função individual.
Exemplo: /functions/v1/manage-users é servido por /home/deno/functions/main/index.ts, não por /home/deno/functions/manage-users/index.ts.
O runtime Deno é iniciado com --main-service /home/deno/functions/main, então main/index.ts é o router.
3. Architecture completa da requisição
`
Browser → Kong (port 443) → functions-proxy (nginx) → functions-1 (Deno :9000)
`
O nginx proxy apenas forwarda para a porta 9000 do Deno sem filtrar auth.
4. Adicionar debug ao main/index.ts
O arquivo main/index.ts contém a lógica de verificação JWT. Para adicionar debug:
`bash
Copiar para local
docker cp deploy-vps-functions-1:/home/deno/functions/main/index.ts /tmp/main_index.ts
Editar localmente (não usar sed com strings contendo +)
python3 -c "
content = open('/tmp/main_index.ts').read()
fazer patch no arquivo
with open('/tmp/main_index.ts', 'w') as f:
f.write(content)
"
Copiar de volta
docker cp /tmp/main_index.ts deploy-vps-functions-1:/home/deno/functions/main/index.ts
`
5. Verificar secret no .env
O .env em /docker/deploy-vps/.env pode ter trailing \n no JWT_SECRET. Corrigir com Python local:
`python
with open('/docker/deploy-vps/.env') as f:
lines = f.readlines()
Encontrar linha JWT_SECRET e remover trailing \n
lines[linha] = 'JWT_SECRET=valor_sem_newline\n'
with open('/docker/deploy-vps/.env', 'w') as f:
f.writelines(lines)
`
6. Forçar recreate do container após corrigir .env
`bash
cd /docker/deploy-vps
docker compose up -d --force-recreate auth
docker compose up -d --force-recreate functions
`
Arquitetura do Edge Runtime (Deno)
Armadilhas
SERVICE_ROLE_KEY JWT: role deve ser "service_role", NÃO "supabase_admin"
PostgREST rejeita role: "supabase_admin" com permission denied to set role "supabase_admin". Use role: "service_role" com is_super_admin: false. Gerar com PyJWT:
`python
import jwt
with open('.env') as f:
for line in f:
if line.startswith('JWT_SECRET='):
secret = line.split('=',1)[1].strip()
svc_key = jwt.encode({
'sub': '00000000-0000-0000-0000-000000000000',
'iss': 'supabase', 'ref': 'rochasales',
'role': 'service_role', # não supabase_admin
'iat': 1780674526, 'exp': 2138836800,
'is_super_admin': False,
'email': 'service_role@rochasales'
}, secret, algorithm='HS256')
Atualizar .env: SERVICE_ROLE_KEY=
`
dbFetch routing: /auth/v1/ dobra para /rest/v1/auth/v1/ — 404
O dbFetch original prefixa /rest/v1 em todos os endpoints. Chamadas /auth/v1/admin/users viram http://kong:8000/rest/v1/auth/v1/admin/users (404). Corrigir:
`typescript
const AUTH_URL = "http://kong:8000";
async function dbFetch(endpoint: string, options: RequestInit = {}) {
const isAuth = endpoint.startsWith("/auth/");
const base = isAuth ? AUTH_URL : SUPABASE_URL;
const path = isAuth ? endpoint : /rest/v1${endpoint};
// ...
}
`
422 email duplicado vira 500 — tratar GoTrue 422 explicitamente
GoTrue retorna 422 para email já existente, mas dbFetch lança como erro → 500. Catch:
`typescript
try { createRes = await dbFetch("/auth/v1/admin/users", {...}); }
catch (e: any) {
if (e.message?.includes("422")) {
return new Response(JSON.stringify({ error: "A user with this email address has already been registered" }), { status: 422, headers: {...} });
}
throw e;
}
`
functions-proxy NÃO está no docker-compose — container avulso
O nginx que faz proxy para o Deno (deploy-vps-functions-proxy) é gerenciado separadamente:
`bash
docker restart deploy-vps-functions-proxy
`
Se Kong ficar reiniciando o container functions em loop, o problema é o healthcheck do Kong falhando porque Deno não está escutando na porta 9000.
Token expira após recreate de container — re-autenticar
Quando docker compose up -d --force-recreate é executado, tokens JWT armazenados no localStorage do navegador se tornam inválidos. Avisar usuário para fazer logout e login novamente após mudanças nos containers.