Sistema Resiliente de Envio de Mensagens — Pilot Status
Meta: enviar até 50.000 mensagens/mês com máxima confiabilidade, zero perda e mínima duplicidade.
Infraestrutura atual: Monolito (Next.js — Frontend + Backend + Worker) · PostgreSQL · Redis · Evolution API (WhatsApp não-oficial) · 3 números (Ash, Brock, Misty) · Docker/Docker Compose · Imagens no GHCR · Servidor único · Experiência AWS disponível.
Funcionalidade planejada: reescrita de mensagens de Marketing com IA (DeepSeek) para variação de conteúdo.
Sumário
- Visão Geral da Arquitetura
- Fluxo Completo de Envio (Passo a Passo)
- Agendamento e Expiração (deliverAt / deliverUntil)
- Diagrama de Estados da Mensagem
- Regras de Negócio Consolidadas
- Mecanismos Atuais de Resiliência
- Pontos de Falha e Gargalos (Inventário Completo)
- Capacidade: Cálculo para 50k/mês com 3 Números
- Plano de Resiliência Máxima (Melhorias)
- Integração com IA (DeepSeek) para Marketing
- Arquitetura de Infraestrutura Recomendada
- Observabilidade e Operação
- Roadmap de Implementação
1. Visão Geral da Arquitetura
┌─────────────────────────────────────────────────────────────────────────┐
│ CLIENTE (API Externa / Dashboard) │
│ POST /api/v1/messages/send → Auth → Rate Limit → Validação │
└──────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ MONOLITO (Next.js — Frontend + Backend + Worker) │
│ │
│ ┌─────────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ MessageService │───▶│ PostgreSQL │ │ Redis (BullMQ) │ │
│ │ .send() │ │ status=QUEUED│ │ Job enfileirado │ │
│ └─────────────────┘ └──────────────┘ └───────────┬────────────┘ │
│ │ │
│ ┌──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ WORKER (processMessage) │ │
│ │ Lock Redis → Renderiza template → [DeepSeek rewrite?] │ │
│ │ → Simula digitação → Chama Evolution API │ │
│ │ → Atualiza DB → Dispara Webhooks do cliente │ │
│ └──────────────────────────────┬──────────────────────────────────┘ │
│ │ │
└─────────────────────────────────┼───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ EVOLUTION API (Gateway WhatsApp) │
│ 3 Instâncias: Ash · Brock · Misty │
│ Cada instância = fila BullMQ separada (messages--{instance}) │
│ Webhook retorna: SERVER_ACK → SENT · DELIVERY_ACK → DELIVERED │
└─────────────────────────────────────────────────────────────────────────┘
Stack tecnológica:
| Componente | Tecnologia | |---|---| | Aplicação | Next.js (Monolito: Front + Back + Worker) | | Banco de dados | PostgreSQL via Prisma | | Fila de mensagens | BullMQ + Redis | | Provider WhatsApp | Evolution API (não-oficial) | | Lock distribuído | Redis (SET NX) | | Retry | BullMQ (backoff fixo 30s, 10 tentativas) | | IA para Marketing | DeepSeek (planejado) | | Container | Docker + Docker Compose | | Registry | GHCR (GitHub Container Registry) | | Infraestrutura | Servidor único (Postgres + Redis + App) |
2. Fluxo Completo de Envio
Fase 1 — Recepção e Validação (Backend)
- Autenticação: header
x-api-key→ApiKeyService.validate()→ resolve tenant/projeto. - Ambiente:
- TEST: permite envio apenas para números do perfil do dev. Limite diário via
TEST_MODE_DAILY_LIMIT. - LIVE: exige
project.productionApproved = true. Template deve estar aprovado para produção.
- TEST: permite envio apenas para números do perfil do dev. Limite diário via
- Template: resolve versão aprovada mais recente (por
templateIdou nome friendly). Valida compatibilidade projeto/ambiente. - Instância WhatsApp:
- Se API key vinculada a
whatsappInstanceId→ instância deve existir e estarOPEN(senão409). - Senão → usa
EVOLUTION_INSTANCE_NAME(instância default). - Templates
MARKETINGnão podem usar o número default → exigem instância própria.
- Se API key vinculada a
- Rate Limit (
RateLimitService.check()):- LIVE: limite mensal (plano + pacotes). Plano FREE: limite diário adicional.
- Auto-Recharge automático se habilitado e saldo abaixo do percentual configurado.
- Opt-In Transacional (
WhatsAppTransactionalOptInService.assertDestinationAuthorized()):- Obrigatório quando: validação habilitada + instância default + ambiente LIVE +
projectIdexiste. - Exceção: templates
OTPno número Pilot Status podem fazer bypass. - Pode ser ignorado com
skipOptInValidation.
- Obrigatório quando: validação habilitada + instância default + ambiente LIVE +
- Cálculo de
deliverUntil(expiração automática por categoria):OTP→ 2 minutosUTILITY→ 1 horaMARKETING→ 4 horas
- Criação do registro no banco:
status = QUEUED,correlationId,deliverBy,deliverUntil. - Enfileiramento no BullMQ:
jobId = messageId,attempts = 10,backoff = fixed/30s,priority(urgente = 1, normal = 5).
Resposta ao cliente: 202 Accepted com { id, correlationId, status: "QUEUED", origin }.
Fase 2 — Processamento pelo Worker
- Lock Redis distribuído:
SET NX EX 60com chavelock:ps:message:{messageId}. Se não adquirir → ignora (sem erro). - Busca da mensagem: verifica existência e
status = QUEUED. Se não QUEUED → ignora (idempotência). - Verificação de expiração: se
deliverUntilultrapassado →FAILED+ webhook do cliente. - Renderização do template: substitui
{{variavel}}no corpo. Suporta texto simples e JSON com botões. - [FUTURO] Reescrita IA: se mensagem de Marketing e flag habilitado → chama DeepSeek para reescrever.
- Validação da instância: determina
evolutionInstance— vem do job, da mensagem, ou da envEVOLUTION_INSTANCE_NAME. - Deduplicação (instância "Pilot Status"):
tryAcquirePilotStatusSendSlot()evita que o mesmomessageIdseja enviado mais de uma vez (proteção contra retentativas do BullMQ). Ao contrário da versão anterior, agora é permitido repetir o conteúdo da mensagem se forem IDs de mensagem diferentes. - Simulação de digitação:
trySimulateWhatsAppTyping()→/chat/sendPresence(best-effort). - Chamada à Evolution API:
- Com botões →
POST /message/sendButtons/{instance} - Sem botões →
POST /message/sendText/{instance}(comlinkPreviewse URL detectada)
- Com botões →
- Verificação de estado: se resposta indica
state=close/connecting→ lança erro de desconexão. - Classificação de erros (heurística por substring):
NotFound→FAILEDterminal, marca instânciaCLOSE.Disconnected→ mantémQUEUED, não queima tentativa. Worker verifica se restam tentativas.- Erro genérico na última tentativa →
FAILED, dispara webhookmessage.failed.
- Sucesso (HTTP 2xx): salva
evolutionKeyId/evolutionInstanceIdno banco. NÃO altera status para SENT — aguarda webhookSERVER_ACK.
Fase 3 — Confirmação via Webhook da Evolution
Handler em /api/internal/webhook:
SERVER_ACK→SENTDELIVERY_ACK→DELIVEREDREAD/PLAYED→READERROR/FAILED→FAILED- Resolução da mensagem: por
messageId, depoisevolutionKeyId+instanceId, depois fallbackevolutionKeyId. - Aplica progressão de status (não regredir
READ → SENT). - Emite webhooks do cliente para
FAILED,DELIVERED,READ.
Fase 4 — Sistemas de Garantia e Monitoramento
Reconciliador de Fila (message-reconciler.ts)
- Frequência: a cada 1 hora (configurável via
MESSAGE_RECONCILER_INTERVAL_MS). - Lock: Redis
ps:message-reconciler:lock, TTL 55s — evita execução concorrente. - Ações sobre mensagens
QUEUED:- Expira mensagens com
deliverUntilultrapassado →FAILED. - Falha mensagens com mais de 7 dias em QUEUED →
FAILED("timeout de entrega"). - Valida dados mínimos:
whatsappInstanceName,templateVersionId,destinationNumber,payload. - Reenfileira mensagens órfãs (job sumiu da fila — ex: Redis reiniciou).
- Promove jobs "delayed" que já deveriam ter sido processados.
- Reprocessa jobs
completedoufailedque ainda estejam QUEUED no banco.
- Expira mensagens com
Healthcheck de Instâncias (all-instances-healthcheck.ts)
- Frequência: a cada 10 minutos via cron no BullMQ.
- Fase 1 – Verificação: para cada instância
OPEN, checaconnectionState+presence.- Se
not_found(404): marcaCLOSEno banco. - Se desconectada: adiciona à lista "disconnected".
- Se
- Fase 2 – Restart: chama
restartWhatsappInstance()para todas em paralelo. - Aguarda:
HEALTHCHECK_SLEEP_MS(padrão 2 min). - Fase 3 – Recheck: se ainda offline → alerta Pushover (dev) + WhatsApp (admin do tenant).
- Deduplicação de alertas: Redis
ps:instance:disconnect-alert-sent:{nome}.
Validação Individual (whatsapp-instance-validate-sweep)
- Frequência: a cada 5 minutos.
- Para cada instância
OPEN, verificaconnectionState+presence. Se inválida → alerta admin.
Sincronização de Workers
- Frequência: a cada 60s (
WHATSAPP_QUEUE_WORKER_REFRESH_MS). - Sincroniza lista de filas ativas com instâncias OPEN no banco.
3. Agendamento e Expiração
deliverAt (Agendamento)
- Data/hora alvo (ISO 8601) a partir da qual a mensagem pode ser enviada.
- Salvo no banco como
Message.deliverBy. - Job enfileirado com
delay = max(0, deliverBy - now). - Mesmo após estar "pronta", o delay aleatório padrão (30–45s) ainda se aplica.
deliverUntil (Expiração)
- Data/hora limite para entrega.
- Se omitido, aplica regras padrão por categoria: OTP (2min), UTILITY (1h), MARKETING (4h).
- Worker e reconciliador verificam antes de processar/reenfileirar.
Reconciliação no Startup (SLA de 10 minutos)
- Se
now > deliverBy + 10 min: marcaFAILED. - Se
deliverBy <= now <= deliverBy + 10 min: reenfileira/promove. - Configurável via
MESSAGE_RECONCILER_INTERVAL_MS.
4. Diagrama de Estados da Mensagem
┌─────────────┐
│ QUEUED │ ◄─── Reenfileirado pelo reconciliador
└──────┬──────┘
│
┌─────────────┼──────────────┐
│ │ │
Expirou Enviou com Erro de envio
deliverUntil sucesso (não-fatal)
│ │ │
▼ ▼ │ (retry BullMQ, até 10x)
FAILED (aguarda webhook) │
│ │
Evolution envia │
SERVER_ACK webhook │
│ │
▼ │
SENT ◄──────────┘
│
DELIVERY_ACK
│
▼
DELIVERED
│
Usuário lê (READ/PLAYED)
│
▼
READ
Garantia prática: sistema é at-least-once (pelo menos uma tentativa). Duplicidade é possível se não houver idempotência ponta a ponta.
5. Regras de Negócio Consolidadas
| Regra | Detalhe |
|---|---|
| Um job por mensagem | jobId = messageId — sem duplicatas no BullMQ |
| Idempotência no worker | Verifica status = QUEUED antes de processar |
| Lock por mensagem | Redis SET NX 60s impede processamento concorrente |
| Concorrência = 1 | Cada fila de instância tem concurrency = 1 |
| Delay aleatório | Após processar: sleep(30–46s) para simular comportamento humano |
| Prioridade | Jobs urgentes: priority = 1, normais: priority = 5 |
| 10 tentativas | attempts = 10, backoff = fixed/30s (~5 min de retry total) |
| Instâncias separadas | Cada instância WhatsApp tem fila e worker próprios |
| MARKETING exige número próprio | Não pode usar instância default/Pilot Status |
| Roteamento por instância | Fila messages--{instance} — todos os tenants da mesma instância compartilham fila |
| Confirmação por webhook | Status SENT só é definido pelo webhook SERVER_ACK da Evolution |
6. Mecanismos Atuais de Resiliência
| Mecanismo | Como funciona |
|---|---|
| Persistência antes do envio | Registro no banco com QUEUED antes de enfileirar |
| Desacoplamento com filas | BullMQ: API não aguarda envio, responde imediatamente |
| Retry automático | 10 tentativas, backoff fixo 30s via BullMQ |
| Smart Wait | Se instância desconectada, não "queima" tentativa — volta para QUEUED |
| Reconciliador | Varredura periódica de mensagens presas em QUEUED |
| Lock distribuído | Redis SET NX evita processamento concorrente |
| Deduplicação Pilot Status | Hash do texto para evitar mesma mensagem para mesmo número |
| Healthcheck de instâncias | Verificação + restart automático a cada 10 min |
| Validação individual | Sweep a cada 5 min para instâncias OPEN |
| Sincronização de workers | A cada 60s, novos workers para novas instâncias |
| Alertas | Pushover (dev) + WhatsApp (admin) para instâncias offline |
7. Pontos de Falha e Gargalos (Inventário Completo)
🔴 Críticos
| ID | Ponto de Falha | Impacto | Cenário |
|---|---|---|---|
| PF-01 | Redis como SPOF | Sistema para completamente | Se Redis cair: nenhum job enfileirado/processado, API retorna 500 |
| PF-02 | Worker crash → duplicata | Mensagem enviada 2x | Job executado na Evolution, mas banco ainda QUEUED → lock expira → reconciliador reenfileira |
| PF-03 | Status SENT depende de webhook assíncrono | Mensagem enviada 2x | Evolution envia mas webhook SERVER_ACK não chega → mensagem fica QUEUED → reconciliador reenvia |
| PF-04 | Sem timeout nas chamadas HTTP | Fila trava, lock expira, duplicidade | fetch() sem AbortController → job preso indefinidamente → lock Redis 60s expira |
🟠 Importantes
| ID | Ponto de Falha | Impacto | Cenário |
|---|---|---|---|
| PF-05 | Webhook do cliente sem retry | Notificações de status perdidas | dispatchCustomerWebhook() falha → apenas loga e segue |
| PF-06 | OTP expira antes do reconciliador | Mensagem OTP nunca chega | TTL 2min, reconciliador roda 1x/hora |
| PF-07 | Instância offline acumula QUEUED por 7 dias | Volume cresce, mensagens expiram | Healthcheck tenta reconectar, mas se falhar, mensagens ficam paradas |
| PF-08 | Sem isolamento de projetos na fila | Starvation entre projetos | Burst de um projeto atrasa mensagens de outro na mesma instância |
| PF-09 | bodyOverride descartado pelo reconciliador | Mensagens com body customizado incorretamente rejeitadas | coercePayload retorna null para payloads com estrutura diferente |
| PF-10 | Lock do reconciliador com TTL curto (55s) | Dupla reconciliação | Reconciliação lenta → lock expira → segundo worker inicia concorrente |
| PF-11 | Contabilização de rate limit depende de sentAt (ACK) | Envio acima do limite | Se webhook atrasar, mensagens não entram na contagem |
| PF-12 | Retenção 0 apaga dados para reconciliação | Mensagem não pode ser reenviada | PII redigida, se job perdido, reconciliador não consegue reconstruir |
| PF-13 | Heurística frágil para "disconnected" | Retry infinito ou falha prematura | Classificação por includes() em texto do erro — falso positivo/negativo |
| PF-14 | Retry fixo 30s (Thundering Herd) | DDoS acidental na Evolution | Se Evolution cai e volta, todas as mensagens retentam ao mesmo tempo |
🟡 Menores
| ID | Ponto de Falha | Impacto | |---|---|---| | PF-15 | DLQ silenciosa (sem alarme) | Falhas passam despercebidas | | PF-16 | Delay aleatório em jobs urgentes do reconciliador | Latência extra desnecessária | | PF-17 | Concorrência=1 gargalo em alto volume | Fila cresce indefinidamente | | PF-18 | Perda de webhook durante deploy/restart | Status "fantasma" (QUEUED/SENT para sempre) |
8. Capacidade: Cálculo para 50k/mês
Números
- 50.000 mensagens / mês = ~1.667/dia = ~70/hora = ~1,16/minuto (média).
- 3 instâncias (Ash, Brock, Misty) = ~556/dia/instância = ~23/hora/instância.
Throughput Atual (concurrency=1 + delay 30-46s)
- Pior caso: 1 mensagem a cada 46s por instância = ~78 mensagens/hora/instância = ~234/hora total.
- Melhor caso: 1 mensagem a cada 30s por instância = ~120/hora/instância = ~360/hora total.
- Por dia (24h): 5.616 a 8.640 mensagens total.
- Por mês (30 dias): 168.480 a 259.200 mensagens total.
Conclusão de Capacidade
✅ Com 3 instâncias e o throughput atual, o sistema suporta 50k/mês COM FOLGA, mesmo considerando que os envios se concentram em horários comerciais (8h–20h = 12h/dia).
Em 12h/dia: 2.808 a 4.320 mensagens/dia × 30 = 84.240 a 129.600/mês → ainda suficiente.
⚠️ Risco: se os envios forem concentrados em picos (ex: 10.000 mensagens de marketing de uma vez), pode levar de 2,7h a 4,2h para processar com 3 instâncias. Mensagens OTP nesse período podem sofrer atraso crítico.
Recomendação para Picos
- Separar filas por categoria (OTP/UTILITY em fila prioritária, MARKETING em fila separada).
- Reduzir delay para instâncias próprias: 15-20s em vez de 30-46s.
- Distribuir marketing entre as 3 instâncias de forma balanceada.
9. Plano de Resiliência Máxima (Melhorias)
🚀 Prioridade 1 — Anti-Duplicidade e Confiabilidade (Semanas 1-2)
MR-01: Estado intermediário DISPATCHED (anti-duplicidade)
Resolve: PF-02, PF-03, PF-18
Após receber HTTP 2xx da Evolution, atualizar imediatamente:
await prisma.message.update({
where: { id: msg.id },
data: {
status: 'DISPATCHED', // novo estado intermediário
dispatchedAt: new Date(),
evolutionKeyId: response.key?.id,
evolutionInstanceId: response.instanceId,
},
});
- Webhook
SERVER_ACKpromoveDISPATCHED → SENT. - Reconciliador respeita grace period (ex: 60 min) antes de reenfileirar
DISPATCHED. - Se grace period expirar sem ACK, consulta Evolution API pelo
evolutionKeyIdantes de reenfileirar.
MR-02: Timeout em todas as chamadas HTTP
Resolve: PF-04
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const response = await fetch(url, { signal: controller.signal, ... });
} finally {
clearTimeout(timeout);
}
Timeouts recomendados:
- Typing/presence: 3s
- Envio Evolution: 15s
- Webhook do cliente: 8s
- DeepSeek API (reescrita): 10s
MR-03: Verificar status na Evolution antes de reenfileirar
Resolve: PF-02, PF-03
// No reconciliador, antes de reenfileirar:
if (msg.evolutionKeyId) {
const status = await checkEvolutionMessageStatus(msg.evolutionKeyId);
if (status === 'delivered' || status === 'sent') {
await prisma.message.update({
where: { id: msg.id },
data: { status: 'SENT', sentAt: new Date() },
});
continue; // NÃO reenfileira
}
}
MR-04: Backoff exponencial com jitter
Resolve: PF-14
// De:
{ backoff: { type: 'fixed', delay: 30_000 } }
// Para:
{ backoff: { type: 'exponential', delay: 5_000 } }
// Resultado: 5s → 10s → 20s → 40s → 80s → 160s → 320s → 640s → 1280s → 2560s
Adicionar jitter (aleatoriedade ±20%) para evitar thundering herd.
MR-05: Retry inteligente por status code
Resolve: PF-13
function classifyError(error: any): 'retry' | 'fatal' | 'disconnect' {
const status = error.response?.status;
if (status === 404) return 'fatal'; // instância não existe
if (status === 400 || status === 422) return 'fatal'; // payload inválido
if (status === 401 || status === 403) return 'fatal'; // auth
if (status === 429) return 'retry'; // rate limit
if (status >= 500) return 'retry'; // erro servidor
if (error.code === 'ECONNREFUSED' || error.code === 'ECONNRESET') return 'disconnect';
return 'retry';
}
⚡ Prioridade 2 — Infraestrutura e Filas (Semanas 3-4)
MR-06: Transactional Outbox Pattern (Redis SPOF)
Resolve: PF-01
Em vez de escrever no Redis diretamente ao receber a requisição:
- API salva mensagem no Postgres (transação) — já faz isso.
- Relay process (novo): lê mensagens novas do Postgres e empurra para Redis.
- Se Redis cair, API continua aceitando. Quando Redis voltar, relay preenche a fila.
// Relay (executar via cron ou loop com polling)
async function relayOutbox() {
const pending = await prisma.message.findMany({
where: { status: 'QUEUED', enqueuedAt: null },
take: 100,
orderBy: { createdAt: 'asc' },
});
for (const msg of pending) {
await queue.add('send-message', buildJobData(msg), { jobId: msg.id, ... });
await prisma.message.update({ where: { id: msg.id }, data: { enqueuedAt: new Date() } });
}
}
MR-07: Separação de filas por categoria (Lane Isolation)
Resolve: PF-08, PF-06
Fila: messages--ash--otp (concurrency=1, delay=5s, priority=máxima)
Fila: messages--ash--utility (concurrency=1, delay=15s, priority=alta)
Fila: messages--ash--marketing (concurrency=1, delay=30s, priority=normal)
- Workers independentes com pesos diferentes.
- OTP nunca é atrasado por burst de marketing.
- Reconciliador de alta frequência (30-60s) para filas OTP.
MR-08: Fila de webhook com retry
Resolve: PF-05
// Fila separada para webhooks do cliente
await webhookQueue.add('deliver', { webhookId, event, payload }, {
attempts: 5,
backoff: { type: 'exponential', delay: 5_000 },
});
MR-09: Redis com persistência AOF + Sentinel
Resolve: PF-01
# docker-compose.yml - Redis com AOF
redis:
image: redis:7-alpine
command: >
redis-server
--appendonly yes
--appendfsync everysec
--maxmemory 512mb
--maxmemory-policy noeviction
volumes:
- redis-data:/data
restart: always
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
Para alta disponibilidade futura: Redis Sentinel ou migrar para AWS ElastiCache.
🔧 Prioridade 3 — Robustez Operacional (Mês 2)
MR-10: Renovação de lock do reconciliador (Watchdog)
Resolve: PF-10
export async function withMessageReconcileLock(params) {
// acquire lock...
const extender = setInterval(() => connection.pexpire(key, ttlMs), ttlMs / 2);
try {
return await params.fn();
} finally {
clearInterval(extender);
// release lock...
}
}
MR-11: Corrigir reconciliador para bodyOverride
Resolve: PF-09
Verificar se payload contém bodyOverride: true e usar fluxo sendRaw ao reenfileirar.
MR-12: Alertas automáticos para DLQ
Resolve: PF-15
worker.on('failed', (job, err) => {
if (job && job.attemptsMade >= job.opts.attempts) {
pushoverService.sendDeveloperAlert({
title: '🚨 Mensagem em DLQ',
message: `Job ${job.id} falhou ${job.attemptsMade}x: ${err.message}`,
});
}
});
MR-13: Circuit Breaker para Evolution API
Resolve: PF-14
class EvolutionCircuitBreaker {
private failures = 0;
private openUntil: Date | null = null;
private readonly threshold = 5; // 5 falhas consecutivas
private readonly cooldownMs = 120_000; // 2 min
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.openUntil && new Date() < this.openUntil) {
throw new Error('Circuit breaker OPEN — Evolution API indisponível');
}
try {
const result = await fn();
this.failures = 0;
this.openUntil = null;
return result;
} catch (err) {
this.failures++;
if (this.failures >= this.threshold) {
this.openUntil = new Date(Date.now() + this.cooldownMs);
}
throw err;
}
}
}
MR-14: Fallback de Instâncias (Roteamento Inteligente)
Se o cliente possui múltiplas instâncias (ex: Ash como primária, Brock como fallback) e a primária desconectar, o sistema re-roteia para uma instância ativa em vez de esperar deliverUntil expirar.
MR-15: Sincronização Ativa de Status (Anti-Perda de Webhooks)
O reconciliador consulta mensagens DISPATCHED/SENT há mais de X minutos sem DELIVERY_ACK. Faz polling ativo na Evolution API para checar status real e atualizar banco.
MR-16: Delay configurável por prioridade
Jobs do reconciliador ou com urgent = true → delayMs = 0.
MR-17: Concorrência aumentada com rate-limit
Para instâncias próprias do cliente: concurrency = 3-5 com rate-limiter (token bucket) baseado nos limites da Evolution API.
10. Integração com IA (DeepSeek) para Marketing
Objetivo
Cada mensagem de Marketing é reescrita pela IA para ser única, reduzindo risco de detecção como spam pelo WhatsApp e aumentando engajamento.
Arquitetura Proposta
Worker recebe job de Marketing
│
▼
Template renderizado com variáveis
│
▼
┌──────────────────────────────────┐
│ DeepSeek API (reescrita) │
│ POST /chat/completions │
│ Prompt: "Reescreva mantendo │
│ sentido e tom. Máx 160 chars." │
│ Timeout: 10s │
│ Fallback: texto original │
└───────────────┬──────────────────┘
│
▼ (texto reescrito ou original se IA falhar)
│
▼
Envio via Evolution API
Regras Importantes
- Fallback obrigatório: se DeepSeek falhar (timeout, erro, rate limit), enviar texto original. Nunca bloquear envio por falha de IA.
- Cache de variações: armazenar últimas N variações geradas para evitar repetição.
- Timeout agressivo: 10s máximo para chamada DeepSeek.
- Rate limit de IA: limitar chamadas ao DeepSeek (ex: 100/min) para controlar custos.
- Validação do resultado: verificar se o texto reescrito mantém variáveis obrigatórias e não excede limite de caracteres.
- Salvar no banco: gravar texto original e texto reescrito para auditoria.
- Flag por template: permitir ativar/desativar reescrita por template, não globalmente.
Modelo de Dados
model Message {
// ... campos existentes ...
originalBody String? // corpo antes da reescrita
aiRewrittenBody String? // corpo após reescrita
aiRewriteModel String? // "deepseek-chat"
aiRewriteLatency Int? // ms
}
Consideração de Volume
- 50k mensagens/mês, supondo 60% marketing = 30k reescritas/mês.
- DeepSeek é muito mais barato que OpenAI — custos estimados baixos.
- Fazer batching se possível (enviar 5-10 textos por chamada).
11. Arquitetura de Infraestrutura
Estado Atual (Servidor Único)
┌─────────────────────────────────────────────┐
│ Servidor Único │
│ ┌───────────┐ ┌──────┐ ┌────────────────┐ │
│ │ PostgreSQL│ │Redis │ │ Monolito │ │
│ │ │ │ +AOF │ │ (Next.js) │ │
│ │ │ │ │ │ Front+Back+ │ │
│ │ │ │ │ │ Worker │ │
│ └───────────┘ └──────┘ └────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ Evolution API (Docker) │ │
│ │ 3 instâncias: Ash, Brock, Misty │ │
│ └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Docker Compose Recomendado (Otimizado para Resiliência)
version: '3.8'
services:
postgres:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_DB: pilot_status
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
redis:
image: redis:7-alpine
restart: always
command: >
redis-server
--appendonly yes
--appendfsync everysec
--maxmemory 256mb
--maxmemory-policy noeviction
--save 60 1000
--save 300 100
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
app:
image: ghcr.io/${GHCR_USER}/pilot-status:${VERSION}
restart: always
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/pilot_status
REDIS_URL: redis://redis:6379
EVOLUTION_API_URL: ${EVOLUTION_API_URL}
EVOLUTION_API_KEY: ${EVOLUTION_API_KEY}
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY}
MESSAGE_RECONCILER_INTERVAL_MS: "300000" # 5 min (mais frequente)
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 2G
cpus: '2.0'
volumes:
postgres-data:
redis-data:
Evolução para AWS (Quando Escalar)
┌──────────────────────────────────────────────────────────┐
│ AWS │
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ RDS │ │ ElastiCache │ │ ECS/Fargate │ │
│ │ PostgreSQL │ │ Redis (Cluster) │ │ App + Worker │ │
│ │ Multi-AZ │ │ Multi-AZ │ │ Auto-scaling │ │
│ │ Backups auto │ │ Failover auto │ │ │ │
│ └─────────────┘ └──────────────────┘ └──────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐│
│ │ CloudWatch: Alertas + Métricas + Logs ││
│ └────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────┘
Vantagens AWS:
- RDS PostgreSQL Multi-AZ: failover automático, backups, point-in-time recovery.
- ElastiCache Redis: elimina SPOF Redis, failover automático.
- ECS Fargate: escalar workers horizontalmente sob demanda.
- CloudWatch: métricas, logs centralizados, alertas.
12. Observabilidade e Operação
Métricas Essenciais
| Métrica | Como medir | Alerta |
|---|---|---|
| Latência QUEUED → DISPATCHED | Diferença dispatchedAt - createdAt | > 5 min média |
| Latência DISPATCHED → SENT | Diferença sentAt - dispatchedAt | > 15 min |
| Taxa de retry | attemptsMade > 1 / total | > 20% |
| Profundidade de fila | BullMQ getWaitingCount() por instância | > 500 |
| Taxa de FAILED | FAILED / total nos últimos 60 min | > 5% |
| Instâncias offline | Contagem de instâncias com state != OPEN | > 0 |
| DLQ | Jobs com attemptsMade >= maxAttempts | > 0 |
| Volume de QUEUED > 30 min | Mensagens QUEUED há + de 30 min | > 50 |
Alertas Automáticos
- Pushover (desenvolvedor): instância offline > 5 min, DLQ, taxa de FAILED > 5%.
- WhatsApp (admin do tenant): instância desconectada, falha de envio crítica.
- E-mail/Slack (futuro): relatórios diários de volume, taxa de entrega, SLA.
Dashboard Recomendado
- BullMQ Board ou Bull Monitor para visualização de filas.
- Endpoint interno
/api/internal/queue-healthcom métricas JSON. - Gráficos: volume/hora, taxa de sucesso, latência P50/P95/P99.
Ferramentas Operacionais
- Reprocessar manualmente: botão no dashboard para reenfileirar mensagem específica.
- Pause sending: pausar processamento por instância (útil durante manutenção Evolution).
- DLQ viewer: listar mensagens em DLQ com motivo da falha e opção "Forçar Reenvio".
13. Roadmap de Implementação
Semana 1-2 (Crítico — Anti-Duplicidade):
□ MR-01: Estado intermediário DISPATCHED
□ MR-02: Timeout em todas as chamadas HTTP (AbortController)
□ MR-03: Verificar status na Evolution antes de reenfileirar
□ MR-04: Backoff exponencial com jitter
□ MR-05: Retry inteligente por status code
Semana 3-4 (Infraestrutura de Filas):
□ MR-06: Transactional Outbox Pattern (Redis SPOF)
□ MR-07: Separação de filas por categoria (OTP/UTILITY/MARKETING)
□ MR-08: Fila de webhook com retry
□ MR-09: Redis com persistência AOF
Mês 2 (Robustez Operacional):
□ MR-10: Renovação de lock do reconciliador (Watchdog)
□ MR-11: Corrigir reconciliador para bodyOverride
□ MR-12: Alertas automáticos para DLQ
□ MR-13: Circuit Breaker para Evolution API
□ Integração DeepSeek para reescrita de Marketing
Mês 3+ (Otimização e Escala):
□ MR-14: Fallback de Instâncias (Roteamento Inteligente)
□ MR-15: Sincronização Ativa de Status
□ MR-16: Delay configurável por prioridade
□ MR-17: Concorrência aumentada com rate-limit
□ Dashboard de saúde das filas (BullMQ Board)
□ Migração para AWS (RDS + ElastiCache + ECS) se necessário
Resumo Executivo
| Aspecto | Atual | Com Melhorias | |---|---|---| | Capacidade | ~168k-259k/mês (3 instâncias) | ✅ Suporta 50k/mês com folga | | Duplicidade | Possível (sem estado intermediário) | Eliminada (DISPATCHED + verificação Evolution) | | Redis SPOF | Sim (parada total) | Mitigado (Outbox + AOF) → Eliminado (AWS ElastiCache) | | Webhook perdido | Mensagem fantasma para sempre | Sincronização ativa + grace period | | OTP latência | Até 2min + delay fila | Fila prioritária dedicada | | Marketing spam detection | Textos idênticos | IA (DeepSeek) reescreve cada mensagem | | Observabilidade | Logs básicos | Métricas, alertas, dashboard, DLQ viewer | | Retry | Fixo 30s (thundering herd) | Exponencial com jitter | | HTTP timeout | Nenhum | 3-15s conforme operação |
Documento gerado em: 2026-03-21
Consolidação de: agendamento-mensagens.md, analise-fluxo-envio-mensagens.md, analise-fluxo-mensagens.md, fluxo-envio-mensagens.md
Contexto: 3 números (Ash, Brock, Misty) · Evolution API · Monolito Docker · PostgreSQL + Redis · DeepSeek IA · Meta 50k/mês