Backend do ERP Bem Comum, modelado como modular monolith. Vários módulos de negócio coabitam o mesmo processo Node, isolados por pasta + prefixo de tabela (ctr_*, fin_*, …) e comunicando-se por eventos via Outbox (ADR-0014/0015). Cada módulo é desenhado para poder ser extraído como serviço independente no futuro, sem refactor traumático.
Stack: Node.js 24 LTS · TypeScript 6.0 (roadmap TS 7 / compilador nativo — ADR-0009) · ESM (
"type": "module",NodeNext) · pnpm 11 · Drizzle ORM 0.45 + mysql2 3.22 (MySQL 8.4 — ADR-0020) · borda HTTP Fastify 5 com Zod contract-first + OpenAPI 3.1 (ADR-0025/0027) · storage S3/MinIO (ADR-0019) · auth próprio com JWT ES256 viajose(ADR-0024).Source of Truth:
handbook/(handbook/architecture/adr/vence tudo). Contexto canônico emCLAUDE.md. Orquestrador, agentes e skills em./.claude/.Regras invariantes: sempre
pnpm, nuncanpm(ADR-0012) · borda HTTP é a UX primária; a CLI embutida foi retirada (ADR-0037) — validação E2E é feita em Hurl (ADR-0074; substituiu o Bruno em 2026-10-06).
Os Bounded Contexts vivem sob src/modules/, cada um com a mesma anatomia (domain/ → application/ → adapters/ → public-api/). A comunicação cross-módulo passa exclusivamente por public-api/ + eventos no Outbox (ADR-0006/0014).
| Módulo | Responsabilidade | Borda HTTP |
|---|---|---|
auth |
Identidade própria + RBAC por permissão, login/refresh (JWT ES256), usuários, papéis, reset de senha, foto de perfil — ADR-0024. | /api/v2/auth, /api/v1/{users,me,approvers} |
contracts |
Gestão de Contratos — agregados Contract/Amendment/Document, ciclo de vida (pending/active/cancelled — ADR-0023/0039), export CSV. Módulo inaugural. |
/api/v2/contracts |
financial |
Financeiro — títulos/payables, baixa manual, conciliação bancária, remessa CNAB240, extrato/timeline, read-model de fornecedor (ADR-0045). | /api/v2/financial |
partners |
Registry de parceiros — colaboradores, fornecedores, financiadores, geografia/território (soft-delete ADR-0035), ACT (ADR-0036) — ADR-0031. | /api/v1/{collaborators,suppliers,financiers,…} |
programs |
Gestão de programas + logo storage S3/MinIO — spec 008, ADR-0033. | /api/v1/programs |
notifications |
E-mail transacional — templates + EmailSender (Nodemailer/Resend), consumidor de eventos de domínio (ADR-0010/0047). |
worker email-dispatch |
budget-plans |
Planos orçamentários — plano, resultado realizado e estrutura de custo; taxonomia do planejável (ADR-0048/0051). | /api/v2/budget-plans |
reports |
Leitura agregada sobre o que os outros módulos escreveram. Não tem domain/ — é consulta, não modelo, e por isso não tem regra própria a proteger. |
/api/v2/reports |
Identificadores em EN (regra invariante de CLAUDE.md §"Idioma"). Strings ao humano e mensagens de erro formatadas em PT.
src/
├── server.ts Entrypoint HTTP — compõe os plugins de cada módulo, graceful shutdown
│
├── shared/ Cross-módulo, puro (sem regra de negócio)
│ ├── primitives/ Result<T,E>, Brand<T,Tag>, exhaustive, immutable
│ ├── kernel/ VOs de domínio compartilhados: cpf, cnpj, money, period, plain-date, user-ref
│ ├── ports/ adapters/ Cross-cutting (Clock + SystemClock/FixedClock)
│ ├── http/ Shell da borda: app.ts, config.ts, errors.ts, reply.ts (ADR-0028)
│ ├── outbox/ Worker genérico de Outbox + tipos (ADR-0015)
│ ├── observability/ correlation id
│ ├── runtime/ last-resort handlers (uncaught/unhandled)
│ └── utils/ csv, date, hash, id, string
│
├── modules/<módulo>/ Bounded Context (auth · contracts · financial · partners · programs · notifications)
│ ├── domain/ PURO — Result<T,E>, branded, Readonly, sem infra
│ ├── application/
│ │ ├── ports/ type contracts (Repository, EventBus, Storage, …)
│ │ └── use-cases/ factory functions (deps) => (input) => Promise<Result>
│ ├── adapters/ Implementações concretas
│ │ ├── http/ plugin Fastify, controllers/DTOs, schemas Zod (composition.ts)
│ │ ├── persistence/ Drizzle + mysql2 (schemas, mappers c/ Result, repos, drivers, migrations)
│ │ ├── outbox/ append transacional de eventos
│ │ └── storage/, export/, … conforme o módulo
│ └── public-api/ Fronteira cross-módulo: http.ts, events.ts, index.ts, permissions.ts, read.ts, migrate.ts
│
├── workers/ Composition roots de processos longos (ADR-0022/0041)
│ ├── supplier-view-projection/ fin_supplier_view ← eventos do partners (ADR-0045)
│ ├── contract-count-projection/ par_contract_count_view ← eventos do contracts (ADR-0046)
│ └── email-dispatch/ notifications consome eventos de domínio → EmailSender (ADR-0047)
│ (+ relays de outbox por módulo: contracts/worker, partners/worker)
│
└── jobs/ Oneshot jobs (ADR-0041)
├── contracts/sweeper/ varredura de ciclo de vida de contratos por tempo
├── financial/supplier-view-backfill/ backfill do read-model de fornecedor
└── migrate/ aplica migrations Drizzle
tests/ modules (mirror de src/) · cleanup (invariantes estruturais) · e2e · etl · infra · workers · jobs …
db/drizzle/ configs do drizzle-kit por módulo (contracts, auth, partners, programs, financial, notifications)
scripts/ ci · claude · e2e (smoke HTTP) · etl · financial · handbook · seed · setup
handbook/ Source of Truth — interno ao repo
├── architecture/adr/ ADRs aceitos (IMUTÁVEIS)
├── reference/<tech>/ typescript · nodejs · drizzle · mysql · mysql2 · docker · pnpm · fastify
│ · fastify-plugins · nodemailer · zod · bruno · magalu-cloud · claude-code
├── domain_questions/, inquiries/, interviews/, reviews/, specs/, runbooks/, …
llms.txt Mapa do acervo em markdown plano, para leitura por IA
.claude/
├── agents/ Especialistas por tecnologia
├── skills/ Técnicas e disciplinas aplicadas
├── rules/ Regras path-scoped (carregam quando o path casa)
├── agent-memory/ Memória entre sessões
└── hooks/ pre-commit-typecheck.sh, block-npm.sh, prettier-write.sh, …
package.json#imports declara subpath imports nativos (Node, sem transpiler):
Imports relativos e via #src/* carregam a extensão .ts (requisito de NodeNext + allowImportingTsExtensions):
import { Money } from '#src/shared/kernel/money.ts';A UX primária é HTTP (ADR-0037). src/server.ts compõe um plugin Fastify por módulo e expõe a API versionada (ADR-0033):
/api/v2/…— modelo greenfield (auth,contracts,financial): plugin direto./api/v1/…— espelho do legado (partners,programs, gestão de usuários/acessos): superfície compatível com o sistema antigo durante a estratégia strangler-fig (ADR-0001).
Características da borda:
- Contract-first com Zod 4 + OpenAPI 3.1 (
fastify-zod-openapi/zod-openapi, ADR-0027): o schema valida a entrada e gera o contrato. Swagger UI via@fastify/swagger/@fastify/swagger-ui. - Hardening com
@fastify/helmet,@fastify/cors,@fastify/rate-limit(limite dedicado para login/refresh). - Auth/RBAC cross-módulo:
requireAuth+authorize/hasPermissionexportados peloauthprotegem as rotas dos demais módulos. - Read/Write split (ADR-0026): cada módulo aceita
*_DATABASE_URL(writer) e*_READER_URL(réplica; ausente → reusa o writer). - Drivers
memory|mysqlpor módulo via env (<MÓDULO>_DRIVER), resolvidos por uma única guarda no boot (src/shared/persistence/module-driver-config.ts, #456). Em produção, driver ausente/inválido oumysqlsem*_DATABASE_URLderruba o boot com exit 78 (EX_CONFIG), listando todos os problemas de uma vez — nunca degrada em silêncio (foi o que deixou #374 e #444 mudos). Fora de produção, degrada para in-memory com aviso por módulo.memoryexplícito é sempre aceito; em produção, avisa.
Sempre
pnpm, nuncanpm(ADR-0012). Versão pinada viapackageManager+ corepack; há hookPreToolUse(Bash)que bloqueianpm.
pnpm install # respeita pnpm-lock.yaml
pnpm install --frozen-lockfile # em CI
# Gate de qualidade — os quatro, nesta ordem
pnpm run typecheck # tsc --noEmit (TypeScript 7, strict completo)
pnpm run format:check # prettier --check .
pnpm run lint # oxlint (.oxlintrc.json, type-aware via oxlint-tsgolint)
pnpm test # tests/**/*.test.ts via node:test (executa .ts direto)
# Servidor + processos de background
pnpm run serve # sobe a borda HTTP (node src/server.ts); config via env
pnpm run dev # overmind start — HTTP + workers juntos (Procfile)
pnpm run job:migrate # aplica migrations
pnpm run job:contracts:sweep # oneshot: varredura de ciclo de vida de contratos
# ⚠️ DEBUG de um worker isolado — NÃO é a stack. São entrypoints anteriores à consolidação
# #407, e não existe `worker:projections` nem `worker:van`: quem os usa como stack sobe
# topologia diferente da de produção, sem worker da VAN. Ver a nota após este bloco.
pnpm run worker:outbox # relay do outbox de contracts (idem :partners)
pnpm run worker:email-dispatch # consumidor de e-mail (notifications)
pnpm run worker:supplier-projection # projeção fin_supplier_view
# Testes de integração (sobem MySQL/S3 via Docker compose --wait) e smoke E2E HTTP
pnpm run test:integration:financial # idem :contracts :auth :partners :programs :notifications :storage :etl …
pnpm run test:e2e:auth # smoke bash contra server real (idem :contracts :collaborators)
# Suíte da borda em Hurl (ADR-0074) — NÃO versionada (api-collections-hurl/ está no .gitignore),
# não é gate, e um clone novo não a tem. Contrato em .claude/rules/api-collections.md
hurl --test --jobs 1 --variables-file api-collections-hurl/_vars.env api-collections-hurl/casos/**/*.hurl
# Migrations (Drizzle Kit) — uma config por módulo em db/drizzle/
pnpm run db:generate # contracts (idem :auth :partners :programs :financial :notifications)
# Secrets locais p/ docker-compose
pnpm run secrets:setup # gera ./secrets/*.txt
# Diário de bordo das sessões (quem caiu sem emitir SessionEnd)
pnpm run logbook # idem --deadDetalhes completos: CLAUDE.md §Gotchas que não se descobre lendo o código.
⚠️ pnpm run devexige oovermindinstalado (brew install overmind) — ele não é dependência do projeto e não há gate que o cobre. Sem ele o comando falha comcommand not found, sem pista de origem.O
Procfilesobehttpe os quatro grupos de worker.
⚠️ VAN_S3_REGIONeVAN_S3_BUCKETnão são opcionais, e não são só do grupovan: sem elas ohttptambém não sobe.buildVanStorage(src/modules/financial/adapters/http/composition.ts:782) fazthrowquando a config não resolve, e é chamado de forma eager na composição dofinancialsobFINANCIAL_DRIVER=mysql(:949) — fail-fast deliberado do ADR-0068. O manifesto declara as duas noenvobrigatório da unidadebackend(src/deploy/workloads.ts:232-233). Logo não existe subconjunto que dispense asVAN_S3_*— nem viaOVERMIND_PROCESSES, nem viapnpm run serve.
⚠️ Um processo que morre leva os outros. O overmind interrompe o grupo inteiro quando um processo sai, a menos que ele esteja emOVERMIND_CAN_DIE. Em config incompleta, isso aparece como "opnpm run devnão sobe", sem apontar qual processo falhou primeiro — ler o stderr do que morreu antes de culpar os demais.
⚠️ A porta é a do seu.env— medido em 14/10/2026: comPORT=3000declarada,overmind startsubiu a borda em127.0.0.1:3000, eGET /healthrespondeu 200. O overmind atribui porta por processo apenas quando a variável não está declarada; o.env.examplegerado a declara, então esse caminho não aparece no uso normal.overmind statuscontinua sendo a forma de conferir.Sem o overmind, a mesma topologia se reproduz à mão — um terminal por grupo, porque nenhum script
pnpminvoca o runner. A ordem importa: o manifesto declaradependsOn: ['migrate', 'sync-permissions']para obackend(src/deploy/workloads.ts:211), e subir a borda sem semear o catálogo devolve 403 mudo em permissão nova (#462):pnpm run job:migrate # 1. schema node --enable-source-maps src/jobs/auth/sync-permissions/run.ts # 2. catálogo de permissões pnpm run serve # 3. a borda HTTP WORKER_GROUP=outbox node --enable-source-maps src/workers/runner/run.ts WORKER_GROUP=projections node --enable-source-maps src/workers/runner/run.ts WORKER_GROUP=email node --enable-source-maps src/workers/runner/run.ts WORKER_GROUP=van node --enable-source-maps src/workers/runner/run.ts
⚠️ Ospnpm run worker:*não substituem isso: são os seis entrypoints standalone anteriores à consolidação #407, mantidos para debug de um worker isolado (ver.claude/rules/jobs-and-workers.md). Não existeworker:projectionsnemworker:van, então quem os usa como stack sobe uma topologia diferente da que roda em produção — e sem nenhum worker da VAN.
compose.yaml sobe o stack local com hardening (read_only, cap_drop, security_opt), e só vale para ambiente local — homologação e produção são operadas por terceiro e provisionadas por taskdef, com as variáveis de ambiente postas à mão na console da AWS (ADR-0068:28).
profile, então docker compose up sobe só a infra — sem erro algum, o que é o modo de falha fácil de confundir com "subiu".
- Infra (sem profile, sobe sempre): MySQL 8.4 e MinIO (storage S3-compat dev, ADR-0019). Mailpit (captura de e-mail dev) exige
--profile mail. - Aplicação: o serviço
http(--profile app) e os workers por grupo —worker-outbox,worker-projections,worker-email(--profile workers) eworker-van(--profile van). - One-shot:
migrateesync-permissions(emapp,workersejobs, porque os três cenários dependem deles);contracts-sweeperesupplier-view-backfill(--profile jobs).
src/workers/runner/run.ts com WORKER_GROUP, consolidados na #407. O Procfile usado por pnpm run dev segue a mesma topologia. Variantes: compose.ci.yaml (CI, só remove o expose de portas) e compose.etl.yaml (ETL).
Trabalho novo não abre ticket de processo: faz a mudança, roda o gate (typecheck + format:check + lint + test), commita. Decisão nova vira ADR; achado fora de escopo vira issue. Não existe pipeline W0→W3, STATE.json nem wave — foram removidos em 2026-08-06.
./.claude/agents/contratos-orchestrator.md— ponto de entrada único: roteia para o agente especialista ou a skill canônica.CLAUDE.md— regras transversais (idioma, hierarquia de fontes, anti-padrões, política de regressão zero).handbook/architecture/adr/— ADRs aceitos (IMUTÁVEIS).
Achado fora do escopo do ticket atual? Não conserte na hora (scope-creep): registre via skill issue-report (ADR-0040).
Cada agente é ancorado num subdir de handbook/reference/ + ADRs vinculantes, invocado pelo contratos-orchestrator — um agente ou uma skill por turno.
| Agente | Tecnologia | Status |
|---|---|---|
contratos-orchestrator |
Roteamento (agente ou skill) | ✅ ativo |
typescript-language-expert |
TypeScript 6 / type system | ✅ ativo |
nodejs-runtime-expert |
Node 24 / ESM / node:test |
✅ ativo |
drizzle-orm-expert |
Drizzle ORM + Drizzle Kit | ✅ ativo |
mysql-database-expert |
MySQL 8.4 (SQL, índices, locks) | ✅ ativo |
mysql2-driver-expert |
Driver mysql2 (pool, auth, TLS) |
✅ ativo |
docker-compose-expert |
Docker / Compose / BuildKit | ✅ ativo |
pnpm-workspace-expert |
pnpm 11 / supply-chain | ✅ ativo |
fastify-server-expert |
Fastify 5 + plugins | ✅ ativo (ADR-0025) |
zod-expert |
Zod 4 (schemas de borda, ADR-0027) | ✅ ativo |
nodemailer-email-expert |
Nodemailer SMTP adapter | ✅ ativo |
security-backend-expert |
Segurança backend (Node/TS/Fastify) | ✅ ativo |
security-frontend-expert |
Segurança frontend (TanStack/React) | ✅ ativo |
As skills cobrem disciplinas aplicadas — domínio, ports & adapters, schema, testes, revisão, segurança, git local, registro de achado — em .claude/skills/, descobertas nativamente pelo Claude Code, cada uma declarando quando usar. A lista é o diretório: nem tabela nem contagem a manter aqui.
throwproibido emdomain/eapplication/.Result<T, E>em vez disso (src/shared/primitives/result.ts).- Sem
class, semthis—Readonly<>types + funções puras + factory functions com deps injetadas. - Branded types para IDs e VOs (
ContractId,Money,Period,Cpf,Cnpj, …). - Discriminated unions +
switchexaustivo comconst _: never = xno default (semthrow). - Imutabilidade absoluta — mudança por cópia (
{ ...prev, status: 'Cancelled' }). import type+ extensões.tsem imports relativos (NodeNext+verbatimModuleSyntax).- Erros são string literal unions EN kebab-case (
'contract-not-active' | 'amendment-pending'), não classes. - MySQL 8.4 único em dev/CI/prod via Docker compose (ADR-0020); lista normativa de features SQL permitidas/proibidas.
- Isolamento de módulo: importar de outro módulo só via
<module>/public-api/(nuncadomain//application/alheios) — ADR-0006. - Idioma: código em EN; documentação (handbook, ADRs,
.claude/) em PT; strings ao humano em PT.
A sintaxe é enforced pelo tsconfig.json — strict completo, verbatimModuleSyntax (exige import type), NodeNext + allowImportingTsExtensions (exige extensão .ts). O resto vive nas regras path-scoped de .claude/rules/.
- Contexto do projeto:
CLAUDE.md(fonte única) + regras path-scoped em.claude/rules/. - Doc consolidada (humanos + IA):
llms.txt. - Decisões formais:
handbook/architecture/adr/— ADR aceito é IMUTÁVEL, e revogá-lo é abrir outro que osupersedes+handbook/CHANGELOG.md. - Domínio de negócio:
handbook/domain_questions/. - Tecnologias:
handbook/reference/<tech>/— cada uma com agente especialista próprio. - Orquestrador + agentes + skills:
.claude/agents/contratos-orchestrator.md.