ADR-001 — Fábrica de Clientes Supabase
| Campo | Valor |
|---|---|
| Status | ✅ Aceito |
| Data | 2026-04-30 |
| Autores | Equipe OLP |
| Relacionado | ADR-011 — categorias operacionais e nomenclatura |
| Implementação | supabase/functions/_shared/supabase-client.ts |
Contexto
O SDK do Supabase aceita 3 modos de autenticação:
- Anon key (RLS ativo, sem usuário) — uso público
- Anon key + JWT do usuário (RLS ativo, com
auth.jwt()) — uso autenticado - Service role key (RLS bypassado) — uso de sistema
Sem padronização, cada Edge Function chamava createClient inline. Isso causava:
- Service role espalhada em código de fluxo de usuário (risco real de bypass acidental de RLS)
- Token extraído ora do
Authorizationheader, ora do cookie, ora ausente - N conexões abertas por isolate Deno sob carga (5k+ clientes em pico)
- Auditoria difícil:
rg createClientretornava ruído misturando casos legítimos e ilegítimos
Decisão
Toda Edge Function obtém clientes Supabase exclusivamente via _shared/supabase-client.ts, que expõe 5 fábricas nomeadas com semântica explícita:
| Fábrica | Chave | Origem do token | RLS | Quando usar |
|---|---|---|---|---|
createSupabaseClient(req) | ANON | Cookie olp_auth | ✅ ativo | 99% — toda operação de usuário autenticado |
createSupabasePublic() | ANON | — | ✅ ativo | Lookup público (slug de escola, banners login) |
createSupabaseSystem() | SERVICE_ROLE | — | ❌ bypassa | Apenas categorias autorizadas (logging, pre-auth, crons) |
createSupabasePortalAuth(req) | ANON | Cookie olp_mural | ✅ ativo | Portal autenticado (aluno/responsável) |
createSupabaseStorage() | SERVICE_ROLE | — | n/a | Apenas .storage.from() |
Regras invioláveis:
- Proibido
createClient(...)inline em qualquer função (regra reforçada por revisão; varredura atual: 0 violações). createSupabaseSystem()é exceção controlada — toda nova chamada exige justificativa em PR e enquadramento em categoria documentada (pre-auth, logging, crons, feature flags pré-migração).- Service role nunca vai para o frontend nem é passada como caller token de Edge Function.
- Singletons (
_publicClient,_systemClient) por isolate para evitar exaustão de conexão.
Alternativas consideradas
A. Cliente único + flag useServiceRole: boolean
Pros: menos arquivos. Cons: trivial passar true por engano; auditoria perde o nome da função na busca; sem semântica para "portal" vs "sistema". Veredicto: rejeitado — segurança preferida sobre brevidade.
B. Wrapper que decide automaticamente baseado no contexto
Pros: chamador "não pensa". Cons: magia implícita; impossível ler o código e saber se RLS está ativo; viola fail-close. Veredicto: rejeitado — explicitness > convenience em camada de segurança.
C. Manter createClient inline, padronizar via lint
Pros: zero abstração nova. Cons: lint regex frágil; não resolve singletons; não tipa a categoria de uso. Veredicto: rejeitado — abstração de 5 funções tem custo trivial.
Consequências
Positivas
rg createSupabaseSystemlista exatamente onde o RLS é bypassado — auditoria O(1).- Novos devs leem o nome da função e entendem a postura de segurança.
- Singleton resolve problema de conexão sob carga sem mudança em chamadores.
- Migração futura (ex.: trocar provedor) altera 1 arquivo.
Negativas
- 5 nomes a memorizar.
- Cliente Portal e Cliente System parecem similares — mitigado por JSDoc detalhado e categorização explícita.
Trade-offs
| Eixo | Custo | Benefício |
|---|---|---|
| Verbosidade | +5 imports possíveis | Auditabilidade total |
| Acoplamento | Edge Functions dependem de _shared/ | 1 ponto de mudança |
| Performance | Singleton por isolate | Evita N conexões em carga |
| Aprendizado | 5 conceitos | Cada um mapeia 1:1 a um caso de uso |
Conformidade
Como verificar:
# Deve retornar 0 (apenas o próprio supabase-client.ts pode chamar createClient)
rg "createClient\(" supabase/functions --type ts \
-g '!**/_shared/supabase-client.ts'Sinais de violação:
import { createClient } from "@supabase/supabase-js"fora de_shared/supabase-client.tsDeno.env.get("SUPABASE_SERVICE_ROLE_KEY")fora de_shared/
Débito técnico conhecido
Varredura em 2026-04-30: zero violações de inline createClient no backend. As únicas chamadas vivem em _shared/supabase-client.ts e em src/integrations/supabase/client.ts (frontend, anon-only, gerado).
Referências
supabase/functions/_shared/supabase-client.tsADR-011 — service_role vs RLS— detalhamento das categorias de uso de service_role- ADR-005 — extração cookie-only do token
- ADR-006 — único caller legítimo de service_role no fluxo CUD
mem://architecture/adr-service-role-vs-rls-standard