Files
DeskcommCRM/ARCHITECTURE.md
T
Rafael MelgaçoandClaude Opus 5 0750e10010 fix(docs): a doc afirmava o mundo de ontem — 11 documentos medidos contra a fonte
Varredura de "afirmações de estado": toda frase que afirma como o mundo ESTÁ e
que portanto pode ter envelhecido. 393 afirmações medidas contra a fonte em 11
grupos de documento, cada uma com o comando que a responde. 166 confirmadas, 227
com problema, 4 vereditos VERDADEIRA derrubados por um passe adversarial.

E a primeira coisa que a varredura derrubou foi uma frase que EU escrevi hoje.

## O pior achado, e é meu

O runbook dizia "**Desde a 1.3.0**, o agente completa o pin sozinho — em até 5
minutos". Falso. O `completar_pin_ausente` entrou em `81b3bd5d` (2026-08-14),
POSTERIOR à v1.3.0 (2026-08-13), e a v1.3.0 continua sendo a tag mais recente:

    $ git show v1.3.0:hostgator-setup-kit/_common.sh | grep -c completar_pin_ausente
    0

Nenhuma instalação existente tem esse comportamento. E o `update.sh` faz
`git checkout "$TARGET_TAG"` — o kit que roda na VPS é o da TAG, não o da `main`.
Quem atendesse um cliente lendo aquela linha diria "espere cinco minutos que se
resolve", e nada aconteceria, para sempre.

O erro é exatamente o que esta sessão passou o dia caçando: **provei presença na
main e afirmei comportamento na versão publicada.** A mesma frase estava no
CHANGELOG, e pior — dentro da seção `## [1.3.0]`, não em `[Não lançado]`. As duas
corrigidas, e a entrada foi para onde pertence.

## O segundo: o aviso que declarava não-provado o que o documento prova

O topo do runbook dizia "ainda não coberto: app contra Supabase real, sessão de
WhatsApp pareada". O §P4 do MESMO arquivo, 340 linhas abaixo, documenta as duas
coisas na produção. Faltava uma palavra — "pelo ENSAIO" — e sem ela o primeiro
parágrafo que qualquer leitor vê mandava refazer trabalho já feito.

## Três documentos mandavam o leitor agir errado

- **`triagem/TRIAGEM.md`**: "Obrigatórios no merge: verify, build-and-size,
  invariants" — três de cinco, faltando `e2e` e `imagens-ok`. É o arquivo que
  DEFINE a triagem, e o CLAUDE.md registra que medir contra a régua errada é o
  modo de falha número um dela. Agora traz o comando, não a lista.
- **`docs/deploy-hostgator/README.md`**: mandava o comprador leigo instalar um
  autenticador e esperar um QR de MFA "no primeiro login". Com o MFA opcional, o
  `bootstrap-owner.ts` grava `mfa_required: false` e essa tela não aparece — o
  leigo concluiria que a instalação falhou. É o guia P0 de primeira impressão.
- **`ARCHITECTURE.md`**: "MFA TOTP forçado pra admin/super-admin", a regra antiga.
  O CLAUDE.md já tinha a nova; a porta de entrada linkada pelo README, não.

## A régua de RAM: duas parcelas medidas, uma herdada

"~150 MB por número de WhatsApp" aparece em SETE documentos que se citam entre si
e **nunca foi medido neste projeto** — vem da síntese do curso WAHA (2026-05). Fui
medir na produção: o contêiner `waha` inteiro em **304,5 MiB com uma sessão
pareada**, contra `mem_limit` de 1280. Um ponto não decompõe baseline e sessão.

A parcela agora está marcada como herdada, com o comando para quem quiser medir.
**O tier recomendado não muda** — a régua dos 4 GB é a soma da stack em operação,
não o WAHA isolado, e os materiais comerciais não foram tocados.

## Segurança: corrigi o fato, não rebaixei o risco

O `docs/threat-model.md` afirma que não há limite de tentativa em login, signup e
aceite de convite. `lib/auth/rate-limit.ts` existe desde 13/08 e cobre os quatro
pontos, com limites nomeados. **Não rebaixei o T1**: isso pede reauditoria com
teste contra instância viva, que o próprio `confidence` do documento diz nunca ter
havido. Corrigi os fatos e marquei a reauditoria como devida.

Uma sub-afirmação sobrevive à letra e morre no espírito, e ficou registrada: a
sonda do doc (`grep lockout|failed_attempts`) devolve ZERO ainda hoje — mas
`rate-limit.ts:158` conta falha de login POR CONTA. A sonda é cega para a defesa
que existe.

## O gate novo, e ele nasceu vermelho pelo motivo certo

`documentacao-aponta-para-o-que-existe.test.ts` vigia a classe inteira nos 36
documentos de AUTORIDADE: link relativo morto, path em crase que não existe, e
frase de pendência que sobreviveu à pendência.

Escopo deliberado: `docs/stories/`, `docs/handoffs/` e `docs/specs/` ficam de fora
— são planejamento, e incluí-los faria o gate nascer com 384 violações e ser
desligado na primeira semana. Nos 36 de autoridade havia UMA, e ela é honesta (o
runbook cita o script e declara que ele não existe): congelada com justificativa.

Ele nasceu vermelho e estava certo — pegou sozinho duas notas de pendência que a
varredura tinha achado e eu ainda não corrigira, em CONTRIBUTING.md e na doutrina.

Quatro sabotagens, cada uma derrubando exatamente 1 dos 4 — inclusive a do escopo
vazio, porque um gate que varre zero arquivo passa verde. Sabotei DEPOIS de deixar
o controle verde: na primeira tentativa o controle já falhava, e as sabotagens não
provavam nada. E `git add -N` antes, porque `git checkout` não restaura arquivo
que o git ainda não conhece — a sabotagem do escopo ficou aplicada e só apareceu
no run seguinte.

## O que NÃO entra aqui

227 achados; apliquei os de gravidade alta cuja consequência é alguém agir errado.
Os de gravidade média e baixa — sobretudo contagens que envelheceram em
`docs/current-state.md` e `docs/harness-audit.md` — ficam listados no relatório,
não corrigidos. Aplicar 227 correções de texto num commit seria trocar prosa velha
por prosa não verificada.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKm2XcTcfgi1vdMcDYSRWa
2026-08-14 16:23:47 -03:00

7.4 KiB

Architecture — DeskcommCRM

Visão de 1 página. Profundidade vive em docs/specs/ e docs/stories/epics/MASTER.md. Mapa de toda a documentação: docs/index.md. Estado real de implementação (o que está pronto vs. incompleto): docs/current-state.md.

Camadas

  • App (Next.js 16 App Router): UI + Route Handlers no mesmo repo. Server Components por default, Client onde precisa de estado. Middleware de borda em proxy.ts (Next 16 renomeou middleware.ts → proxy.ts).
  • DB (Supabase Postgres): RLS em toda tabela tenant-aware via fn_user_org_ids(). Migrations versionadas em supabase/migrations/.
  • Auth (Supabase Auth + @supabase/ssr): cookie SameSite=Strict. Sempre getUser() no server, nunca getSession(). MFA TOTP é opcional e ligado por quem administra — duas políticas independentes que somam (platform_admins.mfa_required e organizations.settings.security.mfa_required), ambas com padrão não exigir; regra pura em lib/auth/politica-mfa.ts. Esta linha dizia "forçado pra admin/super-admin", que era a regra antiga: como o install.sh cria o dono como platform admin, toda instalação self-host recebia um bloqueador de tela cheia logo após o onboarding. Cadastrar e provar são coisas diferentes — quem TEM fator prova na sessão sempre, independente da política.
  • Realtime (Supabase Realtime): postgres_changes para inbox/kanban; broadcast para sinais leves.
  • Storage (Supabase Storage): bucket whatsapp-media privado, URLs assinadas.
  • WhatsApp (WAHA Plus / engine NOWEB): HMAC-SHA512 webhooks; throttle anti-banimento; STOP detection.
  • Filas (event sourcing leve): event_log table + workers via cron. Trigger Postgres NUNCA faz HTTP.
  • Rate limit (Upstash Redis): contador de janela fixa (INCR + EXPIRE) em lib/ai/dispatcher/rate-limit.ts, com fallback in-memory quando Redis falta. ⚠️ Aplicado hoje em apenas 2 pontos (webhook de captação e dispatcher de IA) — o surface público de auth está sem. Ver docs/threat-model.md §T1.
  • AI (Vercel AI Gateway): Anthropic primário, OpenAI backup pra embeddings.
  • Observability (Sentry): beforeSend scrubs PII (CPF/email/phone) e headers sensíveis.

Multi-tenancy

organization_id uuid not null em toda tabela tenant-aware. RLS via helper. Service role bypassa RLS — handlers admin DEVEM filtrar organization_id manualmente, resolvido de fonte confiável (cookie/JWT/webhook secret/path token), nunca do body.

Detalhes: docs/specs/01-spec-platform-base.md.

API REST /api/v1/

  • JSON snake_case. UUID v4. ISO-8601 UTC. Dinheiro _cents + currency.
  • Wrappers ok() / fail() em lib/api/wrappers.ts.
  • Auth dual: cookie session (frontend) ou Authorization: Bearer tok_... (server-to-server).
  • X-Request-Id em toda response, injetado em proxy.ts e correlacionado com o audit log.
  • Idempotency-Key é o contrato pretendido para POSTs de criação; implementado hoje em 1 rota (lgpd/requests/[id]/approve). Ver docs/current-state.md §4.
  • Detalhes: docs/specs/01-spec-platform-base.md §API.

Fluxo de uma requisição

Rota autenticada de tenant (/api/v1/*, 166 handlers):

request → proxy.ts (X-Request-Id, x-pathname; isPublicPath? → bypass;
                    senão valida sessão Supabase via cookie sb-deskcomm-auth)
        → route handler:
             1. Zod valida o input externo
             2. guard: requireRole() | requirePlatformAdmin() | secret/HMAC
             3. resolveActiveOrg() → organization_id de fonte confiável (nunca do body)
             4. query (RLS pelo client de sessão, ou filtro manual de org com service role)
             5. audit() fire-and-forget se houve mutação
             6. ok(data, meta) | fail(code, message, status)

Superfícies não-cookie: /api/v1/cron/* (Bearer INTERNAL_CRON_SECRET, fail-closed), /api/internal/* (x-internal-secret), /api/mcp (Bearer tok_... contra api_tokens), /api/v1/webhooks/* (HMAC + path token). Inventário completo em docs/threat-model.md §1.

Turno do agente de IA: inbound WhatsApp → HMAC + idempotência → event_log → worker → runAgentTurn (RAG + tools MCP) → guardrails before-send → adapter WAHA → handoff humano se gatilho. Diagrama: docs/architecture/agent-turn.html.

Event log + workers

Triggers Postgres emitem linhas em event_log. Workers (cron / Realtime listener) consomem e disparam side effects. Idempotência via unique (organization_id, external_id) + captura code === '23505'.

Workers vivem em workers/ (ai-response, ai-sentiment, rag-indexer, media-persist, media-derive, lgpd-export, lgpd-redact, storage-cleanup, agent-worker), drenados pelos 10 endpoints em app/api/v1/cron/. Contrato: docs/specs/07-spec-events-workers.md.

Integrações externas

Serviço Uso Onde Falta ⇒
Supabase Postgres + Auth + Realtime + Storage lib/supabase/{browser,server,admin}.ts app não sobe (obrigatório sempre)
WAHA Plus (NOWEB) WhatsApp: envio, recebimento, sessões multi-número lib/waha/ canal indisponível; obrigatório em produção
Upstash Redis rate limit + debounce de RAG lib/ai/dispatcher/rate-limit.ts, lib/ai/rag/debounce.ts degrada para memória com warn
Vercel AI Gateway LLM + embeddings (@ai-sdk/anthropic|openai|google) lib/ai/ agente não responde
Nuvemshop e-commerce: pedidos, produtos, webhooks LGPD lib/nuvemshop/ opcional (NUVEMSHOP_ENABLED)
Sentry erros + performance, beforeSend higieniza PII sentry.*.config.ts, instrumentation*.ts opcional
Resend e-mail transacional (convite de time) lib/email/ opcional — o convite cai em copy-to-clipboard
MCP CRM exposto como tools para agentes app/api/mcp/, lib/mcp/ —

Hardening

  • Error boundaries em app/error.tsx, app/app/error.tsx, app/(public)/error.tsx, app/global-error.tsx (Sentry capture + eventId visível).
  • Páginas customizadas 404/403/500/503 com copy PT-BR canônica.
  • Loading skeletons em rotas P0.
  • E2E Playwright + axe-core.
  • Detalhes: docs/stories/epics/EPIC-12-hardening.md.

Onde olhar a fundo