Pular para o conteúdo principal

Arquitetura

Subsistemas

O repositório é dividido em três subsistemas:

  1. API Core + AntiBanGuard (src/) — o backend Fastify/TypeScript descrito nesta página: instâncias de WhatsApp Web, fila de envio, guarda anti-banimento, webhooks e persistência.
  2. Portal de controle (panel/, servido em /panel) — uma SPA React que fala com a API Core para criar instâncias, parear por QR, configurar o anti-ban, acompanhar métricas/alertas e testar envios sem escrever código.
  3. Este site (docs-site/) — Docusaurus estático, cuja referência de endpoints é gerada a partir do OpenAPI exposto pela própria API (fonte única de verdade — ver a seção Referência da API).

Stack real

Confirmado no código (package.json, Dockerfile, prisma/schema.prisma), não por memória:

CamadaTecnologia
RuntimeNode 24 (Dockerfile: FROM node:24-slim)
LinguagemTypeScript, ESM ("type": "module")
HTTPFastify 5 + fastify-type-provider-zod (validação e OpenAPI gerados a partir de schemas Zod)
Sessão WhatsAppwhatsapp-web.js sobre Puppeteer/Chromium headless
Banco de dadosPostgreSQL, via Prisma (@prisma/client)
Fila / cacheRedis + BullMQ
PortalReact (SPA em panel/, buildada com Vite e servida como estático pela própria API em /panel)
Nota sobre versão do Node

O package.json declara "engines": { "node": ">=20" } como piso mínimo, mas o Dockerfile (que é o que roda em produção) usa node:24-slim em todos os estágios de build e runtime. Esta página segue o Dockerfile.

Por que um Chromium por instância (e por que não Baileys)

whatsapp-web.js funciona automatizando uma sessão real do WhatsApp Web dentro de um Chromium headless — exatamente como um humano abrindo web.whatsapp.com num navegador. Isso é uma escolha deliberada: uma sessão de navegador de verdade é o requisito para o comportamento que a API precisa (QR code, eventos em tempo real, superfície de API completa do WhatsApp Web).

O custo é RAM: cada instância é um processo Chromium separado, e cada Chromium headless consome, na prática, ~150–300 MB de RAM. É por isso que existe MAX_INSTANCES (src/infra/config.ts, padrão 10): o WhatsAppManager.create (src/core/WhatsAppManager.ts) recusa (429 max_instances) criar uma nova instância além desse limite, porque cada instância aceita = um Chromium novo = mais RAM reservada no container.

A alternativa mais conhecida, Baileys, implementa o protocolo Multi-Device do WhatsApp diretamente (sem navegador), o que é muito mais leve em RAM — mas não sobe uma sessão real de WhatsApp Web. Como o requisito deste projeto é justamente ter uma sessão de navegador real por trás de cada número, a troca foi RAM por fidelidade de comportamento — e MAX_INSTANCES existe para deixar esse trade-off explícito e configurável por ambiente, em vez de deixar o container ficar sem memória silenciosamente.

Fluxo de um envio proativo

Um envio proativo (proactive: true, o caminho usado por qualquer disparo que não é resposta a algo que o contato escreveu primeiro) passa por várias camadas antes de sair pelo WhatsApp Web de fato:

  1. Rota (src/routes/message.ts, ex.: POST /message/sendText) recebe a requisição e resolve a instância.
  2. Gates do AntiBanGuard (src/core/AntiBanGuard.ts, evaluateProactive), avaliados nesta ordem: opt-in → blacklist → número existe no WhatsApp → circuito de saúde (vermelho pausa envios) → janela de horário → limite diário (com warmup e curva intradiária). Qualquer gate reprovado devolve rejected com o motivo — a mensagem nem chega a ser enfileirada.
  3. Fila (src/services/MessageService.ts + src/core/queues/sendQueue.ts / src/core/sendSlotting.ts): se os gates permitirem, o envio é enfileirado no BullMQ com um espaçamento aleatório de 30–90s em relação ao envio proativo anterior da mesma instância (dobrado quando o estado de saúde está amarelo). A resposta imediata da API é queued (enfileirado) ou scheduled (adiado para a próxima janela/cota) — nunca sent: o envio de fato ainda não aconteceu.
  4. Worker (src/core/queues/sendQueue.ts, processSendJob): quando o job sai da fila, o worker reavalia os mesmos gates do zero, com dados atualizados (contador diário já reflete envios anteriores da mesma rajada, estado de saúde pode ter mudado). Isso fecha uma janela de corrida onde vários envios enfileirados "ao mesmo tempo" leriam o mesmo contador desatualizado e passariam todos pelo limite diário.
  5. whatsapp-web.js: só depois de passar pelo re-gate o worker chama client.sendMessage, que efetivamente fala com a sessão de WhatsApp Web dentro do Chromium da instância.

Um envio reativo (proactive: false, o padrão — resposta a alguém que escreveu primeiro) pula todos os gates do AntiBanGuard e vai direto para a fila com espaçamento zero: é o caminho certo para respostas, e também uma forma de isolar problemas (ver Primeiros passos).

Descrição derivada do código, não de tráfego real

O fluxo acima é exatamente o que o código faz — mas o comportamento do AntiBanGuard sob tráfego real e sustentado (muitas instâncias, dias de warmup, picos de envio) ainda não foi observado em produção. Os limiares existem e são aplicados como descritos; o que não está validado é como eles se comportam num uso contínuo real. Veja o Guia anti-ban para os valores e fórmulas exatos.