Skip to content

ADR-006 — Logging Fire-and-Forget com service_role isolado

CampoValor
Status✅ Aceito
Data2026-04-30
AutoresEquipe OLP
Implementaçãosupabase/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:

  1. Tabela logs_transacoes tem ZERO INSERT policies — nenhum papel (nem administrador) pode inserir via API REST. Isso fecha log injection.
  2. Único caller permitido: registrarLog() em _shared/logging-helper.ts, que usa createSupabaseSystem() (service_role bypassando RLS) — exceção controlada e auditada (ADR-001, Categoria 2).
  3. Fire-and-forget: a chamada é assíncrona e silencia erros. O fluxo principal nunca aguarda nem propaga falha de log.
ts
// 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ário

SSOT 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_OPTIONS mantém filtro do admin sempre alinhado
  • Cache geo evita custo de chamada externa

Negativas

  • "Logs perdidos silenciosos" — se service_role falhar persistente, perdemos dados sem ruído. Mitigação: alerta ntfy em auth-helpers.ts quando padrão de erros emerge.
  • Devs precisam lembrar de chamar registrarLog em cada handler CUD. Mitigação: revisão @audit cataloga ausências (scripts/audit/log-actions-coverage.ts).

Trade-offs

EixoCustoBenefício
Tamper resistanceService role exposta em 1 helperEstruturalmente imune a log injection
Garantia de gravaçãoFire-and-forget pode perder logsLatência zero pro usuário
ManutençãoSSOT em LOG_ACTION_OPTIONS exige disciplinaFiltros admin nunca ficam desatualizados

Conformidade

Como verificar:

bash
# 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_transacoes ganhar INSERT policy
  • registrarLog com await (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.ts manté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.ts
  • src/constants/log-actions.ts
  • docs/security/AUDIT_LOG.md
  • mem://architecture/observability-and-logging-standard
  • mem://architecture/log-actions-catalog-ssot
  • ADR-001 — categoria de uso de service_role