Skip to content

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 PostgresFormatoUso
metaVerifyTokenplaintextHandshake do webhook (GET)
metaAppSecretEncryptediv_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.

metaVerifyToken nã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 cache meta: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 último syncCaches vence no cache wa:webhook:waba:{wabaId} — na prática os appSecret coincidem 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. As InstagramAccount de um Router originam um conjunto espelhado de caches, keyed por igUserId em vez de phoneNumberId: ig:credentials:{igUserId}, ig:router:{igUserId} (o equivalente ao wa:router, lido pelo orchestrator para roteamento inbound do IG), ig:webhook:{igUserId} e ig:comment-triggers:{igUserId}. São mantidos pelo flow-ai-core (via makeInstagramAccountService().warmupCache()) e lidos pelo flow-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-api começa a rejeitar webhooks (sem wa: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/sub

O 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çãoImpacto se falsa
WhatsAppNumber.active = trueCaches não são populados
Router.active = truewa:router e wa:webhook não são gravados
Router.initialFlowId preenchidowa:router não é gravado
Router.metaVerifyToken preenchidometa:verify-token não é gravado
Router.metaAppSecretEncrypted preenchidowa: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 normal

O 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:

  1. Pertença ao mesmo Router (flow.routerId === router.id).
  2. 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

ArquivoPapel
packages/flow-ai-database/prisma/schema.prismaModelos Router, WhatsAppNumber, RouterVariable e relações
services/flow-ai-core/src/services/router.service.tsCRUD + syncCaches, warmupWebhookCache
services/flow-ai-core/src/services/whatsapp-number.service.tsCRUD + populateCaches, warmupCache
services/flow-ai-core/src/services/whatsapp-routing-cache.tsSync de wa:router:{phoneNumberId}
services/flow-ai-core/src/services/whatsapp-webhook-cache.tsSync de wa:webhook e meta:verify-token
services/flow-ai-core/src/services/whatsapp-encryption-cache.tsSync de wa:encryption:{phoneNumberId}
services/flow-ai-core/src/common/crypto.tsAES-256-CBC encrypt/decrypt + geração de par RSA
services/flow-ai-core/src/bootstrap.tsWarm-up síncrono de todos os caches antes do listen()
services/flow-ai-core/src/index.tsinitTelemetry("core") + import dinâmico de bootstrap.js
services/flow-ai-core/src/http/routes/router.routes.tsEndpoints REST do Router
services/flow-ai-core/src/http/routes/whatsapp-number.routes.tsEndpoints REST de números

flow-ai — plataforma proprietária de atendimento via WhatsApp