Três camadas: app, worker e um microsserviço por canal
A decisão de arquitetura que sustentou tudo que veio depois. O núcleo não sabe que WhatsApp existe: cada canal tem seu próprio microsserviço no edge da Cloudflare, que traduz o mundo de fora para um formato único.
A pergunta que definiu a arquitetura foi: o que acontece quando o WhatsApp mudar a API?
Não “se”. Todo provedor de canal muda formato de webhook, política de autenticação e limite de requisição sem pedir licença. Se essa mudança obrigar a mexer no núcleo da plataforma, todo canal novo aumenta o risco de quebrar os que já funcionam.
A resposta foi separar em três camadas com responsabilidades que não se misturam.
As três camadas
┌─────────────────────────────────────────────┐
│ Microsserviços de canal · Cloudflare Workers│
│ whatsapp · instagram · facebook · live chat │
│ mercado livre · olx · shopee · api genérica │
└───────────────────┬─────────────────────────┘
│ evento normalizado
▼
┌─────────────────────────────────────────────┐
│ Worker · Laravel │
│ fila, orquestração, chamada de LLM, │
│ recuperação de contexto, regras de negócio │
└───────────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ App · Laravel │
│ painel, inbox, agentes, base de │
│ conhecimento, agenda, usuários, relatórios │
└─────────────────────────────────────────────┘
O app
É a plataforma que o cliente vê. Painel de controle, inbox das conversas, configuração dos agentes, cadastro de empresa, produtos e serviços, base de conhecimento, agenda, usuários. Laravel puro, síncrono, otimizado para leitura e edição.
O app não processa mensagem. Ele configura e exibe.
O worker
Também Laravel, também nosso, mas com outro papel: é onde todo processamento acontece. Mensagem que entra, resposta que sai, chamada de modelo de linguagem, recuperação de contexto na base de conhecimento, decisão de escalar para humano, gravação do histórico.
Separar isso do app resolveu dois problemas de uma vez. Escala independente: num pico de mensagens eu subo worker sem tocar no painel. E isolamento de falha: modelo de linguagem fora do ar não derruba a interface — a conversa fica na fila e o cliente continua conseguindo usar o sistema.
Os microsserviços de canal
Cada provedor tem o seu, rodando no Cloudflare Workers. Um para WhatsApp, um para WhatsApp Oficial da Meta, um para Instagram Direct, um para Facebook Messenger, um para Live Chat, um para Mercado Livre, um para OLX, um para Shopee, um para Google Meu Negócio, um para a API genérica, um para cada CRM imobiliário integrado.
Cada um faz três coisas e só isso:
- Recebe o webhook do provedor, no formato dele
- Valida a assinatura e responde
200imediatamente - Traduz para o evento normalizado e entrega ao worker
E o caminho de volta, na saída: recebe a resposta normalizada e traduz para o formato que aquele provedor exige.
// Todo canal entrega isto, independente de como o provedor mandou.
interface EventoNormalizado {
canal: 'whatsapp' | 'instagram' | 'mercado-livre' | 'live-chat' | string;
contaId: string;
conversaId: string;
remetente: { id: string; nome?: string; telefone?: string };
conteudo: {
tipo: 'texto' | 'imagem' | 'audio' | 'documento' | 'localizacao';
texto?: string;
midiaUrl?: string;
};
recebidoEm: string; // ISO 8601
idExterno: string; // para idempotência
metadados: Record<string, unknown>; // o que é específico do provedor
}O núcleo consome esse contrato. Ele não sabe que WhatsApp existe — sabe que chegou uma mensagem de texto de um remetente numa conversa. Toda a esquisitice de cada provedor fica presa no microsserviço dele.
Por que Cloudflare Workers para os canais
A pergunta que ouço mais é por que não deixar os webhooks no Laravel.
Latência de resposta ao webhook. Provedor de mensageria espera 200 em
poucos segundos, e alguns desabilitam o webhook depois de N falhas. Worker
responde do datacenter mais próximo, sem cold start relevante e sem depender de
o app estar saudável.
Isolamento de falha por canal. Um bug no parser do Mercado Livre derruba o Mercado Livre. Nada mais. Quando tudo mora no mesmo processo, um payload inesperado pode ocupar worker e afetar todos os canais.
Escala desigual. WhatsApp tem ordens de grandeza mais volume que Google Meu Negócio. Cada canal escala sozinho, e eu não pago por capacidade ociosa nos que recebem pouco.
Deploy independente. Canal novo é um Worker novo. Não toca no app, não toca no worker, não precisa de janela de deploy. Foi essa decisão que permitiu ir de um canal para mais de uma dezena sem reescrever nada — o custo de um canal novo virou aproximadamente constante.
export default {
async fetch(request, env) {
// 1. Assinatura antes de qualquer coisa
if (!(await validarAssinatura(request, env.SEGREDO_WEBHOOK))) {
return new Response('assinatura inválida', { status: 401 });
}
const payload = await request.json();
// 2. Responde imediato: o provedor não espera nosso processamento
const evento = normalizar(payload);
// 3. Entrega ao worker sem bloquear a resposta
request.waitUntil?.(entregarAoWorker(evento, env));
return new Response('ok', { status: 200 });
},
};O waitUntil é o detalhe que faz o padrão funcionar: o provedor recebe o 200
na hora, e a entrega ao nosso worker acontece depois da resposta ter saído.
O custo dessa escolha
Nada sai de graça, e vale registrar o que essa arquitetura cobrou:
Depuração distribuída. Uma mensagem que não chegou pode ter parado em três lugares diferentes. Precisamos de correlação por ID desde cedo — e a gente aprendeu isso do jeito difícil, rastreando à mão nas primeiras semanas.
Três repositórios e três deploys. Mais cerimônia que um monólito. Compensa porque os ciclos são independentes, mas em time pequeno é peso real.
Consistência eventual. Mensagem entra por um caminho e a resposta sai por
outro. Ordem de entrega, duplicata e reprocessamento viraram problema nosso —
por isso o idExterno no contrato: sem idempotência, um retry do provedor faz
o cliente receber a mesma resposta duas vezes.
Se eu estivesse construindo para um canal só, teria feito monólito. A aposta foi que a plataforma ia precisar de muitos canais — e foi o que aconteceu.