ADR-0023: Server-Authoritative Model — Command Handler Único + Events como Auditoria
Context
O backend hoje tem dois caminhos de escrita que coexistem sem contrato:
Caminho de sync (
routes/sync.ts,classSlotService.ts) passa porapplyEvent→ grava evento + projeção em batch atômico D1.Caminho REST online (students, classes, attendance, users, roles, onboarding, church-events) escreve direto na tabela de projeção, sem gravar em
events, sem transação atômica.
Consequências:
O log
eventsé incompleto — só contém dados que chegam por sync ou class-slots. Qualquer feature que confie no log (auditoria LGPD, conflito, replay) está errada para dados online.Escritas não-atômicas:
deleteStudentfazrepo.softDelete()+INSERT student_eventsem dois.run()separados. Um crash no meio corrompe.approveDraft(onboarding) faz multi-tabela sem transação — crash deixa órfãos.A UI não tem
applyEvent;RolesSection.tsxfaz INSERT/UPDATE/DELETE direto no store, sem trilha e sem sync.
Há também três logs parciais (events, student_events, attendance_events) que nenhum código consolida.
Alternatives considered
Opção A — Convergir para Event Sourcing puro
Toda mutação passa por command handler que reidrata agregado, valida invariantes, faz append de evento; a projeção é derivada do log.
Prós: log completo, time-travel, replay garantido. Contras: governança de schema de evento + snapshots custa caro para equipe pequena; a maioria das entidades (attendance, class, nucleus) não é history-as-product; convergir seria mais rewrite do que conserto; a arquitetura já é servidor-autoritativa.
Opção B — CRUD tradicional + audit log onde precisar
Cada entidade decide independentemente se usa log de auditoria.
Prós: simples, sem overhead de ES. Contras: inconsistência entre entidades; a decisão "onde precisar" tende a ser adiada indefinidamente; não resolve o problema do caminho único.
Opção C — Servidor autoritativo + command handler único + events como auditoria (ESCOLHIDA)
Modelo dominante na indústria para offline-first centralizado (PowerSync, Smashing Magazine 2026): a projeção é fonte da verdade; todo caminho de escrita converge num command handler que valida + escreve projeção + grava evento de auditoria em batch atômico; o log é append-only mas o sistema NÃO depende de replay.
Prós: resolve o problema real (caminho único + atomicidade + log completo) sem o overhead de ES puro; o log de auditoria atende LGPD e histórico; a migração é incremental (entidade por entidade). Contras: eventos não são fonte da verdade — auditoria e projeção podem divergir se alguém escrever direto na projeção (mitigado pela matriz de mutação em CI).
Decision
Adotar o modelo servidor-autoritativo: projeção como fonte da verdade, command handler único por entidade, events como log de auditoria atômico (não replay).
Concretamente:
- A projeção (tabelas de domínio no D1) é a fonte da verdade do estado.
- Todo caminho de escrita — online (REST) e sync — converge num único command handler por entidade que, num batch D1 atômico: valida invariante + autorização, escreve projeção e grava evento de auditoria.
- A tabela
eventscontinua como log de auditoria append-only, mas o sistema NÃO depende de replay para reconstruir estado. - Conflito de sync: role-weight + timestamp, derivado de
ROLE_HIERARCHY.
Esta decisão é registrada como parte do refactor em docs/big-fat-refactor-01/refactor-plan.md.
Gates de decisão (resolvidos aqui, moldam fases seguintes)
Gate 1 — Log de auditoria: unificar em events ou manter tabelas por entidade?
Decisão: Unificar em events. As três tabelas parciais (events, student_events, attendance_events) convergem para events com coluna entity_type como discriminador. A estrutura atual de events (0010) já suporta isso; as tabelas por entidade são redundantes.
Consequência: Todo command handler da Fase 4 escreve em events (não em student_events/attendance_events). As tabelas legadas são dropadas após a convergência completa.
Gate 2 — Aggregate boundary do onboarding
Decisão: "Um draft + suas N crianças + seu responsável" é um único agregado com fronteira transacional. O draftId + estado PENDING é a chave de idempotência.
Consequência: approveDraft vira um command handler que faz tudo num batch atômico (cria students, cria user, cria user_roles, cria guardian_links, atualiza draft). Reprocessar é seguro: se o draft já está APPROVED, retorna os mesmos IDs.
Gate 3 — Convenção de rollback por entidade
Decisão: Manter o caminho de escrita antigo no lugar, apenas desconectado, por um release. Apagar só depois de o novo provar-se em produção.
Consequência: Duplicação temporária de código em cada entidade da Fase 4; reverter é uma mudança de roteamento (flag/rota), não um rewrite reverso.
Gate 4 — Arquitetura de módulos + licenciamento
Decisão: Monorepo único com BSL cobrindo tudo; licença como enforcement (signed license files), não separação de repo. Código do módulo vive em modules/packages/ (concluído: de-submodule em 2026-07-07).
Consequência: Contrato de plugin deve ser infra-agnóstico; registerMigrations deve funcionar e ser gated por entitlement; remover duplicação hardcoded do módulo no core. Registrado em detalhe em docs/big-fat-refactor-01/refactor-plan.md Parte 5.
Consequences
Positivas
Um único caminho de escrita → invariantes de negócio são garantidas em todo fluxo
Log de auditoria completo → LGPD, histórico de mudanças em dados de menor
Atomicidade por batch D1 → sem estado parcial em crashes
Convergência mensurável: a matriz de mutação (T2) prova entidade a entidade
Negativas
Mais código no curto prazo: cada entidade ganha um command handler dedicado
Duplicação temporária (rollback convention) até o próximo release
eventscomo auditoria, não replay: se no futuro for necessário time-travel, será preciso backfill ou migração adicional
Neutras
ROLE_WEIGHTmove para@neemias/permissions(Fase 1) — é correção de bug, não decisão arquitetural novaChurch-events e onboarding entram no modelo ou permanecem CRUD explícito — decidir na Fase 4/4b conforme o aggregate boundary