ADR-006 — Logging Fire-and-Forget com service_role isolado
| Campo | Valor |
|---|---|
| Status | ✅ Aceito |
| Data | 2026-04-30 |
| Autores | Equipe OLP |
| Implementação | supabase/functions/_shared/logging-helper.ts, tabela logs_transacoes |
Contexto
A plataforma precisa de audit trail completo (LGPD + compliance):
- Toda operação CUD (Create/Update/Delete) registrada
- Diff de campos (antes vs depois)
- IP, geolocalização (via Cloudflare headers), user-agent
- Resistente a tampering: usuário malicioso não pode inserir entradas falsas
Trade-off: logging não pode quebrar o fluxo. Se INSERT INTO logs falhar, o usuário não pode receber 500 ao salvar um aluno.
Decisão
Logging segue três princípios invioláveis:
- Tabela
logs_transacoestem ZERO INSERT policies — nenhum papel (nemadministrador) pode inserir via API REST. Isso fecha log injection. - Único caller permitido:
registrarLog()em_shared/logging-helper.ts, que usacreateSupabaseSystem()(service_role bypassando RLS) — exceção controlada e auditada (ADR-001, Categoria 2). - Fire-and-forget: a chamada é assíncrona e silencia erros. O fluxo principal nunca aguarda nem propaga falha de log.
// Padrão em toda Edge Function
await persistirAluno(...);
registrarLog(req, { // sem await — fire and forget
usuario_id: user.id,
acao: 'gestao_alunos.create',
alteracoes: gerarAlteracoes(antes, depois),
}).catch(() => {}); // erro de log nunca afeta usuárioSSOT do catálogo de ações
src/constants/log-actions.ts exporta LOG_ACTION_OPTIONS — toda nova registrarLog exige entrada correspondente no mesmo PR (memória log-actions-catalog-ssot). É essa lista que alimenta o filtro do admin.
Diff helper
gerarAlteracoes(antes, depois) produz JSONB diferencial — armazenamos apenas o que mudou, não o estado inteiro. Reduz custo de storage e simplifica leitura no admin.
Geolocalização
Cabeçalhos cf-ipcountry, cf-region, cf-iata (Cloudflare Worker) são lidos sem chamar API externa. Cache local em ip_geo_cache evita reprocessamento.
Alternativas consideradas
A. Tabela com policy INSERT WITH CHECK (auth.uid() = usuario_id)
Pros: nenhuma exceção de RLS. Cons: usuário pode forjar acao: 'admin.deletar_tudo'. Log injection trivial. Veredicto: rejeitado — audit trail tem que ser autoridade.
B. Logs em serviço externo (Datadog, Logtail)
Pros: ferramentas prontas. Cons: PII fora de fronteira controlada (LGPD complica); custo; latência adicional. Veredicto: rejeitado por ora — logs_transacoes + Postgres é adequado para volume.
C. Logging síncrono (await)
Pros: garantia de persistência. Cons: 1 lentidão de banco = 1 erro pro usuário em operação que já foi feita. Inaceitável. Veredicto: rejeitado.
D. Trigger Postgres em cada tabela CUD
Pros: impossível esquecer de logar. Cons: trigger não tem contexto de IP/user-agent/origem; impossível diferenciar ação semântica (importar vs criar); dificulta migrations. Veredicto: rejeitado — log semântico é responsabilidade da camada de aplicação.
Consequências
Positivas
- Log injection é estruturalmente impossível (não há policy de INSERT)
- Falha de log nunca degrada UX
- Catálogo
LOG_ACTION_OPTIONSmantém filtro do admin sempre alinhado - Cache geo evita custo de chamada externa
Negativas
- "Logs perdidos silenciosos" — se
service_rolefalhar persistente, perdemos dados sem ruído. Mitigação: alertantfyemauth-helpers.tsquando padrão de erros emerge. - Devs precisam lembrar de chamar
registrarLogem cada handler CUD. Mitigação: revisão@auditcataloga ausências (scripts/audit/log-actions-coverage.ts).
Trade-offs
| Eixo | Custo | Benefício |
|---|---|---|
| Tamper resistance | Service role exposta em 1 helper | Estruturalmente imune a log injection |
| Garantia de gravação | Fire-and-forget pode perder logs | Latência zero pro usuário |
| Manutenção | SSOT em LOG_ACTION_OPTIONS exige disciplina | Filtros admin nunca ficam desatualizados |
Conformidade
Como verificar:
# logs_transacoes não pode ter INSERT policy
psql -c "SELECT polname FROM pg_policy WHERE polrelid = 'logs_transacoes'::regclass AND polcmd = 'a';"
# Esperado: zero linhas
# Edge Function nova com CUD deve chamar registrarLog
rg "registrarLog\(" supabase/functions/<nova>Sinais de violação:
logs_transacoesganhar INSERT policyregistrarLogcomawait(deve ser fire-and-forget)- Ação não cadastrada em
LOG_ACTION_OPTIONS
Débito técnico conhecido
- Cobertura de logging:
scripts/audit/log-actions-coverage.tsmantém checagem CI. - Algumas operações de leitura crítica (export de planilha) ainda não logam — debate ativo: SELECT geralmente não loga, mas exports de PII deveriam (backlog).
Referências
supabase/functions/_shared/logging-helper.tssrc/constants/log-actions.tsdocs/security/AUDIT_LOG.mdmem://architecture/observability-and-logging-standardmem://architecture/log-actions-catalog-ssot- ADR-001 — categoria de uso de service_role