Arquitetura
Subsistemas
O repositório é dividido em três subsistemas:
- 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. - 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. - 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:
| Camada | Tecnologia |
|---|---|
| Runtime | Node 24 (Dockerfile: FROM node:24-slim) |
| Linguagem | TypeScript, ESM ("type": "module") |
| HTTP | Fastify 5 + fastify-type-provider-zod (validação e OpenAPI gerados a partir de schemas Zod) |
| Sessão WhatsApp | whatsapp-web.js sobre Puppeteer/Chromium headless |
| Banco de dados | PostgreSQL, via Prisma (@prisma/client) |
| Fila / cache | Redis + BullMQ |
| Portal | React (SPA em panel/, buildada com Vite e servida como estático pela própria API em /panel) |
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:
- Rota (
src/routes/message.ts, ex.:POST /message/sendText) recebe a requisição e resolve a instância. - 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 devolverejectedcom o motivo — a mensagem nem chega a ser enfileirada. - 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) ouscheduled(adiado para a próxima janela/cota) — nuncasent: o envio de fato ainda não aconteceu. - 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. whatsapp-web.js: só depois de passar pelo re-gate o worker chamaclient.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).
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.