Appearance
Router — entidade central de configuração
O Router é o agregado raiz da plataforma: tudo que existe no sistema pertence a um Router. Ele é o responsável por definir qual flow executar, quais canais estão ativos, quais agentes recebem tickets e quais segredos são usados para se comunicar com a Meta.
O que o Router agrega
Router
├── initialFlowId ← flow executado na primeira mensagem (relação 1:1, @unique)
├── metaVerifyToken ← token para validar handshake do webhook Meta
├── metaAppSecretEncrypted ← secret para validar assinatura HMAC do webhook
├── llmCredentialId ← credencial LLM usada por todos os agentes de IA deste Router
├── tz ← fuso IANA (default "America/Sao_Paulo") p/ agenda de login dos agentes
├── logoUrl, sentimentConfig, copilotConfig, helpdeskAgentPrefix{Enabled,Template}
│
├── Canais / conversas
│ ├── WhatsAppNumber[] ← números da Meta Business conectados
│ ├── InstagramAccount[] ← contas do canal Instagram
│ ├── WebChatChannel[] ← canais de webchat
│ ├── WhatsAppFlow[] ← formulários interativos (WhatsApp Flows Meta)
│ ├── MessageTemplate[] ← templates da Meta
│ ├── ChannelTypeMapping[] ← conversão de conteúdo multi-canal
│ ├── Chat[] / ContactOptIn[]
│ └── Campaign[] ← disparos em massa
├── Flows / eventos
│ ├── Flow[] ← definições de fluxos conversacionais
│ ├── EventDefinition[]
│ └── ImportDraft[]
├── Helpdesk
│ ├── Queue[] ← filas de atendimento humano
│ ├── QueueDistributionRule[] ← regras de roteamento de tickets
│ ├── AgentGroup[] / Agent[]
│ └── DeskActionCategory[] / DeskAction[]
├── Agentes de IA
│ ├── AiAgent[] / Guardrail[] / SystemPrompt[] / AgentRagDocument[]
│ └── LlmCredential (via llmCredentialId)
├── Integrações
│ └── RouterIntegration[]
└── RouterVariable[] ← variáveis estáticas (opcionalmente criptografadas)O Router é o agregado raiz: no schema Prisma ele fanea para ~24 coleções filhas (
schema.prisma:165-201). O diagrama acima agrupa as principais por domínio; consulte o schema para a lista completa.
Um Router isolado não faz nada: ele precisa de ao menos um WhatsAppNumber e um Flow publicado com initialFlowId apontando para ele. Sem initialFlowId, novas mensagens chegam, são persistidas no Chat, mas nenhum flow é iniciado.
Segredos: fronteira de criptografia
O Router centraliza dois segredos da Meta:
| Campo no Postgres | Formato | Uso |
|---|---|---|
metaVerifyToken | plaintext | Handshake do webhook (GET) |
metaAppSecretEncrypted | iv_hex:ciphertext_hex (AES-256-CBC) | Validação de assinatura HMAC (POST) |
O metaAppSecretEncrypted nunca sai decifrado via HTTP — nem nas respostas da API. O flow-ai-core decifra o valor na memória apenas para gravá-lo no Redis. O flow-ai-meta-api lê o valor já decifrado do Redis sem jamais tocar no Postgres ou na chave de criptografia.
metaVerifyTokennão é@unique(schema.prisma:161). Múltiplos Routers podem compartilhar o mesmo verify token quando todos usam o mesmo Meta App — cenário típico de Embedded Signup. O cachemeta:verify-token:{token}é keyed pelo próprio token, então writes idempotentes são seguros. Pela mesma razão, quando vários Routers usam o mesmo WABA, o últimosyncCachesvence no cachewa:webhook:waba:{wabaId}— na prática osappSecretcoincidem por virem do mesmo Meta App.
Por que cifrar no banco se vai para o Redis em plaintext? O Redis é memória volátil — se reiniciar, os dados somem e o warm-up os repõe. O Postgres é o estado durável. Se o banco vazar (backup, log de auditoria), os secrets estão protegidos. O Redis é um cache operacional interno, sem exposição externa direta.
O mesmo princípio vale para os accessToken dos WhatsAppNumber e as chaves RSA de criptografia de payload.
Os caches Redis que o Router origina
Todo CRUD em Router ou em seus números filhos dispara sincronização imediata de caches. O flow-ai-core é o único serviço que escreve nesses caches — todos os outros leem.
meta:verify-token:{verifyToken}
json
{ "routerId": "rtr_abc" }Lido por flow-ai-meta-api no handshake GET do webhook para identificar qual Router validou aquele token.
wa:webhook:{phoneNumberId}
json
{
"phoneNumberId": "1234567890",
"routerId": "rtr_abc",
"appSecret": "decifrado_em_plaintext"
}Lido por flow-ai-meta-api para validar a assinatura HMAC de cada POST de webhook.
wa:webhook:waba:{wabaId}
json
{
"wabaId": "9876543210",
"routerId": "rtr_abc",
"appSecret": "decifrado_em_plaintext"
}Espelho por WABA do cache de webhook, gravado só para números ativos com wabaId. Lido pelo flow-ai-meta-api para validar eventos WABA-level (ex.: status de templates), que chegam sem phoneNumberId.
wa:router:{phoneNumberId}
json
{
"phoneNumberId": "1234567890",
"routerId": "rtr_abc",
"initialFlowId": "flw_xyz",
"sessionExpiryMs": 86400000
}Lido pelo flow-ai-orchestrator quando uma nova sessão precisa ser criada — é aqui que o sistema descobre qual flow executar para aquele número. O sessionExpiryMs é opcional (só presente quando o Router define um timeout de sessão customizado).
wa:credentials:{phoneNumberId}
json
{
"phoneNumberId": "1234567890",
"accessToken": "EAAB...",
"displayName": "Empresa XPTO",
"wabaId": "9876543210"
}Lido pelo flow-ai-meta-api para autenticar chamadas à Meta Cloud API ao enviar mensagens.
wa:encryption:{phoneNumberId}
json
{
"phoneNumberId": "1234567890",
"privateKeyPem": "-----BEGIN ENCRYPTED PRIVATE KEY-----...",
"passphrase": "hex_passphrase"
}Lido pelo flow-ai-meta-api para decifrar payloads recebidos de WhatsApp Flows (formulários interativos).
Canal Instagram — família
ig:*paralela. AsInstagramAccountde um Router originam um conjunto espelhado de caches, keyed porigUserIdem vez dephoneNumberId:ig:credentials:{igUserId},ig:router:{igUserId}(o equivalente aowa:router, lido pelo orchestrator para roteamento inbound do IG),ig:webhook:{igUserId}eig:comment-triggers:{igUserId}. São mantidos peloflow-ai-core(viamakeInstagramAccountService().warmupCache()) e lidos peloflow-ai-ig-api. Ver o guia do canal Instagram.
Warm-up no boot
O flow-ai-core popula todos os caches antes de começar a aceitar tráfego HTTP:
typescript
// services/flow-ai-core/src/bootstrap.ts (o index.ts só faz initTelemetry("core") + import dinâmico)
await makeRouterService().warmupWebhookCache() // ← wa:webhook + wa:webhook:waba + meta:verify-token
await makeWhatsAppNumberService().warmupCache() // ← wa:credentials + wa:router + wa:webhook + wa:encryption
await makeInstagramAccountService().warmupCache() // ← ig:credentials + ig:router + ig:webhook
await makeInstagramCommentTriggerService().warmupCache()// ← ig:comment-triggers
await makeWhatsAppFlowService().warmupCache() // ← wa:flow
await warmupIntegrationServerCaches(new Crypto()) // ← integration:server
await warmupToolCaches() // ← tool:
await warmupLlmCredentialCaches() // ← llm:credential
await warmupObservabilityConfigCache() // ← observability:config
await warmupAiAgentCaches() // ← agent:
// só então: fastify.listen()Por que warm-up síncrono antes do listen? Se o flow-ai-core aceitar tráfego com caches vazios, o flow-ai-meta-api ficaria sem credenciais para enviar mensagens e sem app secret para validar webhooks. Como o warm-up pode levar segundos (centenas de números × consultas × cifragem), a decisão foi bloquear o boot até completar. O risco de downtime no boot é menor que o risco de processar mensagens sem caches.
Se o flow-ai-core reiniciar enquanto os outros serviços estão ativos:
flow-ai-meta-apicomeça a rejeitar webhooks (semwa:webhook) e não consegue enviar (cache miss de credenciais → entry sem ACK → retry automático).- Assim que o warm-up termina, os caches são repopulados e o tráfego retoma sem perda de mensagens (as entries permaneceram na pending list).
Sincronização em CRUD
Cada operação de escrita dispara syncCaches() imediatamente após a persistência no Postgres:
Criar Router
POST /routers
└─ router.service.create()
├─ Cria no Postgres (com metaAppSecret cifrado se fornecido)
├─ Cria fila padrão ("geral") + regra de distribuição padrão (priority=999)
└─ syncCaches(routerId)
├─ Para cada número do router:
│ └─ setWaRouter(phoneNumberId, { routerId, initialFlowId })
└─ setMetaVerifyToken(verifyToken, { routerId })
setWaWebhook(phoneNumberId, { routerId, appSecret [decifrado] })Um Router recém-criado não tem números — syncCaches é chamado mas não grava nada nos caches de número. Os caches de número só são populados quando um WhatsAppNumber é criado e vinculado.
Atualizar Router
PATCH /routers/:id
└─ router.service.update()
├─ Guarda previousVerifyToken (antes de sobrescrever)
├─ Atualiza no Postgres
└─ syncCaches(routerId, previousVerifyToken)
├─ Se verifyToken mudou:
│ └─ deleteMetaVerifyToken(previousVerifyToken) ← limpa o token antigo
└─ setMetaVerifyToken(newVerifyToken, { routerId })O previousVerifyToken evita orphan entries no Redis: se o token mudar de "abc" para "xyz", meta:verify-token:abc seria mantido no Redis indefinidamente sem o delete explícito.
Remover Router
DELETE /routers/:id
└─ router.service.delete()
├─ Valida que Router não tem flows nem números (não pode deletar com filhos)
└─ deleteMetaVerifyToken(verifyToken)
(wa:router e wa:webhook já foram limpos quando os números foram removidos)Criar/Atualizar/Remover WhatsAppNumber
POST /whatsapp/numbers (ou PATCH /:id)
└─ whatsapp-number.service.create()
├─ Cria no Postgres (com accessToken cifrado)
└─ populateCaches(number)
├─ setWaCredentials(phoneNumberId, { accessToken [decifrado] })
├─ syncWhatsAppRoutingCache() → setWaRouter()
├─ syncWhatsAppWebhookCaches() → setWaWebhook() + setMetaVerifyToken()
├─ syncWhatsAppEncryptionCache() → setWaEncryption()
└─ publishWaCredentialsInvalidate(phoneNumberId) ← pub/subO publishWaCredentialsInvalidate notifica via Redis Pub/Sub qualquer serviço que mantiver credenciais em memória local para invalidar seu cache imediatamente, sem esperar TTL.
Se um número for desativado (active: false), todos os seus caches são deletados:
deleteWaCredentials(phoneNumberId)
deleteWaRouter(phoneNumberId)
deleteWaWebhook(phoneNumberId)
deleteWaEncryption(phoneNumberId)
publishWaCredentialsInvalidate(phoneNumberId)Condições para um número estar operacional
Um WhatsAppNumber só entra nos caches (e portanto só pode receber e enviar mensagens) se todas as condições abaixo forem verdadeiras:
| Condição | Impacto se falsa |
|---|---|
WhatsAppNumber.active = true | Caches não são populados |
Router.active = true | wa:router e wa:webhook não são gravados |
Router.initialFlowId preenchido | wa:router não é gravado |
Router.metaVerifyToken preenchido | meta:verify-token não é gravado |
Router.metaAppSecretEncrypted preenchido | wa:webhook não é gravado |
Isso significa que um número pode estar active=true mas não operacional se o Router ainda não tiver o webhook configurado. O flow-ai-meta-api rejeitará o webhook desse número por ausência de wa:webhook.
Embedded Signup — conexão simplificada
O fluxo de Embedded Signup conecta um número WhatsApp Business sem exigir configuração manual de tokens:
POST /routers/:id/whatsapp/connect
{ code: "FB_LOGIN_CODE", ... }
└─ embeddedSignupService.connect()
├─ Troca code → token de longa duração (Meta OAuth)
├─ Inscreve app no WABA
├─ Registra número com PIN
└─ whatsAppNumberService.upsertFromEmbeddedSignup()
├─ Cria ou atualiza WhatsAppNumber
└─ populateCaches() ← mesmo fluxo de CRUD normalO upsert valida que, se o número já existe no banco, ele pertença ao mesmo Router — um número não pode ser migrado entre Routers pelo Embedded Signup.
initialFlowId — restrições e validação
O initialFlowId de um Router deve apontar para um flow que:
- Pertença ao mesmo Router (
flow.routerId === router.id). - Tenha ao menos uma publicação ativa (
activePublicationId !== null).
Tentar definir um initialFlowId que aponte para flow de outro Router ou para flow sem publicação retorna erro 400. Isso evita que sessões novas sejam criadas com referência a um flow que o engine não consegue carregar.
Arquivos relevantes
| Arquivo | Papel |
|---|---|
packages/flow-ai-database/prisma/schema.prisma | Modelos Router, WhatsAppNumber, RouterVariable e relações |
services/flow-ai-core/src/services/router.service.ts | CRUD + syncCaches, warmupWebhookCache |
services/flow-ai-core/src/services/whatsapp-number.service.ts | CRUD + populateCaches, warmupCache |
services/flow-ai-core/src/services/whatsapp-routing-cache.ts | Sync de wa:router:{phoneNumberId} |
services/flow-ai-core/src/services/whatsapp-webhook-cache.ts | Sync de wa:webhook e meta:verify-token |
services/flow-ai-core/src/services/whatsapp-encryption-cache.ts | Sync de wa:encryption:{phoneNumberId} |
services/flow-ai-core/src/common/crypto.ts | AES-256-CBC encrypt/decrypt + geração de par RSA |
services/flow-ai-core/src/bootstrap.ts | Warm-up síncrono de todos os caches antes do listen() |
services/flow-ai-core/src/index.ts | Só initTelemetry("core") + import dinâmico de bootstrap.js |
services/flow-ai-core/src/http/routes/router.routes.ts | Endpoints REST do Router |
services/flow-ai-core/src/http/routes/whatsapp-number.routes.ts | Endpoints REST de números |