Skip to content

ADR-0044: Núcleo portável — Cloudflare como plataforma de deploy, não modelo de programação ​

Status: accepted Date: 2026-09-11 Deciders: @barateza + sessão de grilling (big-fat-refactor-03) Amends: ADR-0028 (fronteiras de persistência), ADR-0039 §1 (backend-primary) Tags: [portability, ports, adapters, durable-objects, d1, cloudflare, concurrency, event-store, session-coordinator, adr-0028, adr-0039]

Context ​

O Neemias roda inteiro sobre Cloudflare: Workers (HTTP), D1 (estado), Durable Objects (fan-out de feed) e R2 (objetos). Cloudflare não é o problema — é uma plataforma de deploy excelente e barata. O problema é outra coisa:

O domínio fala Cloudflare.

Um detalhe de plataforma que atravessa services/, events/ e o modelo mental dos agentes custa duas vezes: uma para escrever, e outra — a cara — para sair. Se amanhã o D1 ou o R2 deixarem de servir, a migração não é um projeto de infraestrutura, é um rewrite.

Evidência verificada no repositório em 2026-09-11 (não é impressão, é medição):

ObservaçãoMedida
Arquivos em workers/src que importam @cloudflare/workers-types59
services/checkinService.ts1 127 linhas, importa D1Database, e mistura geração de código HMAC, configuração, validação, SQL de capacidade, criação de evento e fan-out de feed
events/eventStore.ts expõe mecanismo, não capacidadeexecuteProjection(sql, params) / executeBatch(stmts)
Durable Object coordenando sessãonenhum — os 3 DOs existentes são fan-out de feed (ADR-0043)
Constraint de unicidade em (entity_type, entity_id, version)não existe (só o índice não-único idx_events_entity)

Emenda (2026-09-12): a última linha desta tabela deixou de valer. A medição acima fica como foi feita, em 2026-09-11; o que mudou desde então está aqui.

O B2 (migrations/0016_events_unique_entity_version.sql) criou UNIQUE(entity_type, entity_id, version) em events e derrubou o índice não-único idx_events_entity. O B3 passou a classificar a recusa do banco: uma violação do índice composto vira VERSION_CONFLICT, e um event_id repetido vira replay idempotente. Os dois andam juntos porque, sem a classificação, a corrida que o índice fecha passaria a responder 500 no lugar de 409 — a constraint sozinha trocaria a duplicata silenciosa por um erro de servidor. A auditoria prévia (B1) achou 0 grupos duplicados nas duas bases.

A tentação errada, ao ver isso, é criar uma abstração de plataforma: um Platform monolítico, um ORM, um "repository genérico" para cada tabela. Isso troca um acoplamento por outro, mais caro de manter. O ADR-0028 já ratificou três padrões de persistência legítimos e deliberados; este ADR não os substitui. Ele nomeia onde fica a fronteira de portabilidade — e, principalmente, impede que ela se mova por acidente.

Decision ​

1. Três invariantes ​

Estas três frases são a decisão. O resto do ADR é consequência delas.

text
Invariante 1:
D1 é o estado persistente autoritativo.

Invariante 2:
Durable Objects coordenam concorrência com escopo de sessão, mas não definem
estado de domínio.

Invariante 3:
Código de negócio/aplicação depende de interfaces de capacidade (ports),
não de APIs de runtime da Cloudflare.

2. Direção de dependência ​

text
                    HTTP / Worker
              routes, auth, headers
                         │
                         ▼
                    Application
                  use cases / services
                         │
                ports (interfaces)
                         │
        ┌────────────────┼────────────────┐
        ▼                ▼                ▼
  Persistence      Coordination      Object-store
        │                │                │
        ▼                ▼                ▼
    D1 adapter       DO adapter        R2 adapter
    PG adapter   Postgres-lock adpt.  S3 adapter

O que é proibido, e a regra que a lint mecânica aplica:

text
PERMITIDO:                        PROIBIDO:

domain / application / ports      domain / application / ports
        ↓                                  ↓
interfaces de capacidade          D1Database / DurableObjectNamespace
                                           / R2Bucket

Tipos específicos de Workers terminam na fronteira de infraestrutura.

3. Ports pequenos e focados — nunca um Platform ​

workers/src/ports/:

text
attendanceRepository.ts    operações de negócio (getActiveCheckIn, recordCheckIn…)
                           NUNCA execute(sql) / query<T>(…)
capacityRepository.ts      get / adjust — o repositório lê/escreve estado;
                           "posso fazer check-in?" é comportamento de domínio
eventStore.ts              getById / getAggregateVersion / append / appendBatch
sessionCoordinator.ts      execute<T>(sessionId, operation)
eventDispatcher.ts         publish(event)
objectStore.ts             put / get / delete
clock.ts                   now()

Regra de desenho: somente criar port onde a dependência é consequente. Um port para cada tabela é o não-objetivo (§8).

4. SessionCoordinator é o seam mais importante — e o mais fácil de errar ​

ts
export interface SessionCoordinator {
  execute<T>(sessionId: string, operation: () => Promise<T>): Promise<T>;
}

A aplicação não pede "um Durable Object". Ela pede "serialize esta operação com escopo de sessão" — que é um requisito genérico de sistemas distribuídos, não uma feature da Cloudflare. As implementações:

text
ImmediateSessionCoordinator     testes / deploy simples (sem plataforma)
DurableObjectSessionCoordinator Cloudflare: namespace.idFromName(sessionId)

Duas armadilhas, ambas explicitamente rejeitadas:

  1. Não colocar a transação D1 dentro do SessionCoordinator. Um coordinator.transaction(sessionId, cb) faria o coordenador passar a ser dono da semântica de persistência. A separação é: o coordinator garante ordenação; o repositório garante persistência; o adaptador D1 compõe os dois onde fizer sentido.
  2. O DO é fino. Ele é um mutex distribuído com identidade endereçável, não "o servidor de Session". SessionCoordinatorDO não valida aluno, não conhece papel, não gera código diário, não escreve D1, não gerencia fila de espera e não publica SSE.

5. A correção nunca depende do DO ​

Adicionar UNIQUE(entity_type, entity_id, version) (migração), após auditar demo/produção por duplicatas e limpar de forma determinística se existirem.

text
DO   →  execução ordenada
D1   →  aplica o invariante

Se o DO for contornado — problema de deploy, migração, ferramenta administrativa, bug, ou outro caminho de API — o banco continua protegendo a correção. Correção que depende só do DO é correção que some no primeiro incidente.

6. Enforcement mecânico ​

scripts/no-cloudflare-in-core.sh (gate docs, portanto em todo commit):

  • Regra A (absoluta, sem baseline): nenhum arquivo em workers/src/{domain, application,ports} importa @cloudflare/workers-types. Esses diretórios são o destino do núcleo portável; hoje estão vazios, então a regra é verde por construção e assim permanece.
  • Regra B (ratchet, CF_CORE_BASELINE em scripts/thresholds.sh = 26): a contagem de arquivos em workers/src/{services,events} que importam o runtime Cloudflare não pode aumentar. São o fonte da migração; acoplamento novo ali é deriva, e o número só desce.

A detecção reusa scripts/count-tokens.sh (o scanner que ignora comentários) em vez de grep: um grep não distingue código de prosa, e este próprio ADR cita o especificador do módulo em texto.

Zonas deliberadamente excluídas, por serem casas legítimas de Cloudflare (§7): routes/, middleware/, adapters/, do/, db/, feeds/, storage/, modules/, index.ts, router.ts.

7. O que permanece Cloudflare-específico — e está certo ​

text
workers/src/routes/         handlers HTTP
workers/src/middleware/     pipeline, idempotência, rate-limit
workers/src/do/             classes Durable Object
workers/src/adapters/       implementações dos ports
workers/src/db/             bindings D1
workers/src/feeds/          fan-out SSE (ADR-0043)
workers/src/storage/        EntityRepository D1
wrangler.toml               configuração de plataforma

A única exigência é que esses módulos apontem para dentro:

text
adaptador Cloudflare  →  port          ✅
domínio               →  adaptador     ❌

8. Não-objetivos ​

  • Sem migração para ORM. SQL cru não é o problema de lock-in.
  • Sem "hexagonal em todo lugar". Port só onde a dependência é consequente.
  • Sem repository genérico por tabela. D1EntityRepo fica para CRUD comum.
  • Sem explosão de pacotes. Nada de 40 pacotes minúsculos.
  • Sem substituir SSE. A arquitetura de feed DO está boa.
  • Sem DO por entidade. A fronteira de coordenação é sessionId.
  • Sem DO como fonte da verdade. D1 permanece autoritativo.
  • Sem Queue no caminho crítico de presença. Fila é para efeito colateral assíncrono; "esta criança pode entrar?" não vai para fila.

9. Relação com ADR-0028 e ADR-0039 ​

  • ADR-0028 permanece válido. Os três padrões (D1 direto, EntityRepository<T>, EventStore + applyEvent) continuam legítimos e não são unificados aqui. Este ADR acrescenta uma coisa que o 0028 não cobria: as implementações D1 desses padrões passam a ficar atrás de ports/ + adapters/. E corrige um vazamento que o 0028 descreveu mas aceitou: a interface de EventStore expõe mecanismo (executeProjection/executeBatch), não capacidade. O reshape dessa interface é o PR-03 do big-fat-refactor-03 e exigirá um amendment ao 0028 — não agora.
  • ADR-0039 permanece válido e não é reaberto. Backend-primary, D1 autoritativo, escritas REST via applyEvent, stack offline decoplada por flag: tudo decidido lá, com VITE_ONLINE_ONLY=true já em produção (IJCP) e demo. A "retirada do proxy SQL do frontend" é execução de uma direção já decidida, não arquitetura nova. A decisão §7 do 0039 (o neemias-db legado é apagado no flip; a ilha é o único estado local) é a leitura vigente de "manter Dexie" — este ADR não reabre isso.

10. Nome e sequenciamento ​

O esforço se chama big-fat-refactor-03 porque big-fat-refactor-02 já existe (.specs/features/big-fat-refactor-02/spec.md, issues #637–#642) e trata de outra coisa: simplificação da infraestrutura de qualidade (ADR-0037), não arquitetura.

Sequenciamento em 3 lanes, detalhado em .specs/features/big-fat-refactor-03/spec.md:

text
Lane A  parar o vazamento de tempo, publicar o Modo Fácil v1
Lane B  piso de correção: unicidade de versão de agregado +
        fan-out de feed desacoplado do commit
Lane C  seams de portabilidade (ports → ImmediateSessionCoordinator →
        SessionCoordinatorDO por último, com o piso da Lane B como rede)

A ordem é deliberada: a Lane C é a mais sedutora e a menos urgente. Nenhum PR da Lane C entrega valor visível ao usuário; as Lanes A e B entregam.

Consequences ​

Positivas ​

  • A direção de dependência passa a ser verificada mecanicamente, não por disciplina.
  • Migrar de plataforma vira um projeto de infraestrutura limitado: reimplementar ports/ sob adapters/ (Postgres/S3/coordenação) sem tocar regras de presença, permissões, schemas, eventos de domínio, protocolo de sync, semântica de conflito, relatórios ou LGPD.
  • A Lane B melhora a correção antes de qualquer DO existir, o que também significa que a adoção do DO não é um evento de risco.
  • O ratchet CF_CORE_BASELINE=26 dá um número honesto de progresso.

Negativas ​

  • Um indireção a mais em troca de portabilidade que talvez nunca seja usada. Aceito conscientemente: o custo é um arquivo de interface por capacidade, não um framework.
  • O ratchet pode incomodar quando alguém precisar, legitimamente, tocar services/ — nesse caso o caminho é o port, que é exatamente o comportamento desejado.
  • Ports mal desenhados podem virar indireção sem profundidade. A mitigação é a regra de desenho do §3 (operação de negócio, nunca SQL) e o não-objetivo do §8.

Riscos ​

  • Ratchet virar letra morta. Se CF_CORE_BASELINE for aumentado sem ADR, a regra morre. Mitigação: o script diz explicitamente que aumentar exige amendment.
  • A Lane C começar antes da Lane B. Correção que depende só do DO é o cenário que o §5 existe para impedir. Ordem: migração de unicidade primeiro.
  • Deriva entre este ADR e o ADR-0028. O amendment do 0028 (reshape do EventStore) está declarado e adiado; se o PR-03 acontecer sem ele, o 0028 fica contradizendo o código.

References ​

  • .specs/features/big-fat-refactor-03/spec.md — spec do esforço e das 3 lanes
  • scripts/no-cloudflare-in-core.sh — enforcement das regras A e B
  • scripts/count-tokens.sh — scanner comment-aware reusado pela lint
  • ADR-0028 — Persistence Pattern Boundaries (três padrões legítimos; não substituído)
  • ADR-0039 — Online-Only Default Mode (backend-primary; VITE_ONLINE_ONLY=true em produção)
  • ADR-0037 — Simplify Quality Infrastructure for Solo Dev + LLM (é o "big-fat-refactor-02")
  • ADR-0043 — One Definition Per Feed (os DOs existentes são fan-out, não coordenação)
  • docs/big-fat-refactor-01/architecture-notes.md — evidência original sobre os dois syncEventSchema divergentes, o event sourcing ilhado e o contrato de plugin quebrado

Distribuído sob licença MIT.