communications-domain-roadmap

# Communications Domain Roadmap

## Objetivo

Descrever a direção-alvo para o Communications Service como central de
comunicação da plataforma Ailian.

Esta página é conceitual. Ela documenta a evolução planejada antes da
implementação.

## Estado atual

Hoje o serviço funciona como motor operacional de notificações:

- recebe `POST /internal/notifications`;
- aplica `notification_preferences`;
- cria `notifications`, `notification_recipients` e `notification_deliveries`
  no PocketBase externo;
- enfileira deliveries em Redis/BullMQ;
- processa envio por provider;
- atualiza status técnico no PocketBase.

Esse desenho reduziu a escrita direta dos produtores no PocketBase, mas ainda
mantém o domínio persistente de notificações fora do Communications.

As notificações comuns seguem neste caminho durante a transição. Isso inclui o
uso do domínio/identidade atual da Ailian quando a comunicação for da própria
Ailian para usuários ou clientes Ailian. A mudança de sender identity e
obrigatória principalmente para envios customer-visible em nome de um cliente,
como campanha, bulk, mensagens 1:1 comerciais e outros casos em que o remetente
percebido deve ser o cliente.

## Direção-alvo

O Communications deve ser promovido para dono do domínio de comunicação.

| Área | Dono alvo |
| --- | --- |
| Usuários, instâncias, entitlement, billing state | Core/Control Plane |
| Lead, deal, pipeline, campanha, audiência, copy, aprovação | CRM/Frappe via CRM Service |
| Consentimento comercial de lead/contato/cliente externo | CRM/Frappe via CRM Service |
| Inbox, preferências, remetentes, templates operacionais, delivery ledger, status e replies | Communications |
| Provider de email, contas de provider e sending pools | Communications |
| Execução de bulk em alto volume | Executor de bulk atrás do Communications |

## Mapa de contexto

```mermaid
flowchart LR
  Browser["Browser"] --> BFF["Nuxt/BFF"]
  BFF --> Core["Core/Control Plane"]
  BFF --> CRM["CRM Service"]
  BFF -->|"alvo / chamadas internas"| Comms["Communications Service"]
  BFF -.->|"leitura/realtime atual"| PB[("PocketBase atual/projeção")]
  CRM --> Frappe["Frappe CRM"]
  CRM --> Comms
  Comms -->|"adapter / transição"| PB
  Comms --> PG[("Postgres alvo")]
  Comms --> Redis[("Redis/BullMQ")]
  Comms --> EventBus["EventBusPort<br/>Redis hoje / NATS alvo"]
  Comms --> Providers["EmailProviderAdapter<br/>Resend / SMTP / SES / SMTP2GO"]
  Comms --> OtherProviders["Chatwoot / WebPush"]
  Comms --> Bulk["Executor de bulk"]
```

O browser não deve chamar o Communications diretamente. Para a inbox atual, o
front pode continuar usando o caminho já existente com PocketBase/realtime até
que exista uma alternativa equivalente pelo BFF ou por API interna.

## Serviços citados

- `Nuxt/BFF`: fronteira server-side do produto. Autentica o usuário, valida
  escopo e chama serviços internos em nome do front.
- `Core/Control Plane`: dono de usuários, instâncias, entitlement, billing state
  e autorizações globais.
- `CRM Service`: fronteira de acesso ao domínio comercial e ao Frappe CRM. É o
  dono de leads, deals, campanhas, audiência, timeline e consentimento
  comercial externo.
- `Frappe CRM`: motor/armazenamento do CRM, acessado por trás do CRM Service.
- `Integration Service`: referência operacional para o padrão HMAC
  serviço-serviço já usado na plataforma. Não é dono do domínio de comunicação.
- `Communications Service`: dono-alvo de política de comunicação, preferências,
  sender identities, templates operacionais, delivery ledger, inbox e writeback.

## Subdomínios internos

### Notification Inbox

Modelo de notificações user-visible:

- item de inbox;
- destinatário;
- estado de leitura, visto, arquivado e ação;
- contadores por usuário/instância;
- navegação e agrupamento.

### Communication Policy

Política aplicada antes de qualquer envio:

- preferências por usuário, instância, serviço, tópico e canal;
- quiet hours;
- opt-in/opt-out de usuário interno;
- supressões técnicas;
- bypass permitido apenas para comunicações obrigatórias.

Consentimento comercial de destinatário externo não deve ser fonte canônica do
Communications. Para leads, contatos e clientes externos, o CRM é a autoridade;
o Communications enforça a política no envio e publica eventos de volta quando
captura unsubscribe, bounce, complaint ou supressão técnica.

Durante a transição, as preferências atuais provisionadas e lidas via
`notification_preferences` continuam válidas. Hooks ou rotinas existentes do
PocketBase não devem ser removidos até que exista um `PreferencePort` ativo,
com defaults e reconciliação equivalentes dentro do Communications ou do Core.

### Sender Identity

Resolução de remetente e domínio:

- remetente default da Ailian;
- remetente por cliente/instância;
- domínio verificado;
- registros DNS exigidos pelo provider;
- status de verificação e última checagem;
- reply-to;
- provider account;
- finalidade permitida, como transacional, operacional ou marketing/bulk;
- fallback seguro.

Regra central: clientes não enviam mensagens customer-visible usando domínio da
Ailian. Comunicações da Ailian podem usar domínio da Ailian; comunicações do
cliente para seus leads, contatos ou clientes precisam usar uma identidade de
envio verificada para aquele cliente/instância. Ausência de identidade válida
deve bloquear o envio com erro de configuração.

O payload de envio não deve aceitar `from` arbitrário como fonte de verdade. O
`from` deve resolver para uma `SenderIdentity` aprovada pelo Communications.

Estado implementado nesta fase: existe `EmailSendingIdentityPort`. Com
`DATABASE_URL`, o adapter lê `communications.sender_identities` no Postgres. Sem
Postgres, usa o adapter estático configurado por `EMAIL_SENDING_IDENTITIES`. O
fallback `RESEND_DEFAULT_FROM` preserva notificações atuais da Ailian/Aleia.
Quando `payload.email.from` aponta para domínio de cliente, o Resend provider
valida se há identidade registrada, com escopo compatível e `status=verified`;
se não houver, o envio falha sem retry.

### Email Provider Abstraction

O Communications deve tratar Resend, SMTP, SMTP2GO, Amazon SES ou outro provedor
como adapters técnicos, não como modelo de domínio.

Conceitos-alvo:

- `EmailProviderAdapter`: contrato técnico para enviar email, validar domínio,
  receber webhooks e normalizar erros/status.
- `ProviderAccount`: conta, credencial ou subconta operacional de um provider.
- `SendingPool`: pool de envio associado a finalidade, reputação, limites e
  fallback.
- `SenderIdentity`: remetente aprovado, resolvido por instância, domínio,
  finalidade e canal.

Resend permanece como adapter inicial para email transacional da própria Ailian.
O contrato novo não deve depender de campos específicos do Resend, para permitir
troca futura por SMTP2GO, Amazon SES, SMTP próprio ou outro provider.

Separação recomendada de pools:

- transacional Ailian;
- transacional em nome do cliente;
- operacional de baixa criticidade;
- marketing/bulk;
- sandbox/teste.

Essa separação evita misturar reputação, rate limit, custo e risco operacional
de emails críticos com campanhas ou envios em nome de clientes.

### Template Registry

Catálogo de templates controlados pelo Communications:

- templates transacionais/operacionais;
- templates WhatsApp/HSM aprovados;
- variáveis obrigatórias;
- versão;
- provider/canal;
- status de ciclo de vida, como `draft`, `review`, `published`, `archived`;
- histórico de publicação e autoria.

Copy criativa de campanha continua no CRM/campanha. O Communications registra a
versão/artefato usado e valida se o envio é permitido.

Templates publicados devem ser imutáveis. Alterações criam nova versão para que
deliveries antigos possam continuar apontando para o template exato usado no
envio.

Os templates locais existentes em código devem ser tratados como sementes ou
adapter inicial do registry, não como um contrato paralelo definitivo. A
migração esperada é:

1. manter renderização local como compatibilidade;
2. importar ou semear templates atuais no registry;
3. publicar versões imutáveis;
4. fazer novos envios apontarem para `templateVersionId`;
5. remover dependência direta do template em código apenas quando o registry
   estiver cobrindo preview, validação, publicação e rollback operacional.

Estado implementado nesta fase: existe `EmailTemplateRegistryPort`. Com
`DATABASE_URL`, o adapter lê o catálogo e versões publicadas no Postgres. Sem
Postgres, usa o adapter local. O `ResendProvider` não chama mais diretamente o
renderer local; ele usa a porta do registry. Os templates atuais foram seedados
como `v1` com `artifact.renderer=local`, preservando o payload dos produtores.

Geração assistida por IA fica fora do escopo inicial. A direção esperada é um
serviço separado de IA/model gateway, responsável por:

- chamadas a modelos;
- contabilização de consumo;
- políticas de prompt;
- guardrails;
- auditoria de uso.

Quando esse gateway existir, ele pode gerar drafts de templates. O
Communications continua dono do catálogo, validação, aprovação, publicação,
renderização e uso dos templates em envio real.

### Delivery Ledger

Estado operacional de entrega:

- delivery request;
- delivery por canal;
- tentativa;
- provider response;
- status;
- retry;
- correlation id;
- business action id.

Para bulk, o ledger deve suportar `bulk_delivery_runs` agregados e ponteiros para
telemetria detalhada no executor de volume.

## Campanha e CRM/Frappe

Campanha deve nascer no domínio do CRM:

- `Campaign`;
- `CampaignRun`;
- audiência/snapshot;
- copy;
- aprovação;
- timeline;
- relação com lead/deal/contact.

O CRM não deve enviar direto, chamar Resend/Chatwoot diretamente ou usar Frappe
como canal público. Quando uma campanha ou ação 1:1 precisa sair, o CRM chama o
Communications com contexto Ailian estável.

Para alto volume, o Communications pode devolver uma autorização selada para um
executor de bulk:

- `campaignRunId`;
- `senderIdentityId`;
- `templateVersionId` ou referência do artefato de campanha;
- `suppressionSnapshotId`;
- provider/executor autorizado;
- limites e validade;
- callback/event stream esperado.

## Consentimento externo

Fluxo recomendado:

1. CRM cria campanha, audiência e snapshot de destinatários.
2. CRM aplica sua regra canônica de consentimento comercial.
3. CRM solicita ao Communications autorização/envio com `campaignRunId`,
   `correlationId`, `senderIdentityId` e referência do snapshot.
4. Communications revalida supressões técnicas e políticas de canal no momento
   do envio.
5. Unsubscribe, bounce, complaint ou nova supressão capturados no delivery geram
   evento para o CRM atualizar a autoridade do contato.

O Communications pode hospedar endpoints técnicos de unsubscribe por causa do
link de email, mas o estado canônico de consentimento comercial deve voltar para
o CRM.

## Fronteira de acesso

O browser não chama o Communications diretamente. Chamadas de produto passam por
Nuxt/BFF ou por outro serviço interno que autentica o usuário e valida o escopo
antes de chamar o Communications.

Chamadas serviço-serviço devem usar HMAC como direção-alvo, reutilizando o
padrão já operado no Integration Service sempre que possível:

- header `X-Integration-Key-Id`;
- header `X-Integration-Timestamp`;
- header `X-Integration-Signature`;
- assinatura `v1=<hex_hmac_sha256>`;
- mensagem canônica com versão, key id, timestamp Unix, método HTTP,
  path+query e SHA-256 do body bruto;
- janela de replay inicial de 300 segundos;
- idempotency key quando houver side effect.

O HMAC protege principalmente contra vazamento/reuso simples de bearer token,
replay de requests, impersonação de serviço e adulteração de body em chamadas
com efeito colateral. Ele não substitui rede privada, autorização por escopo,
rotação de chaves, auditoria e rate limit.

## Superfície de API alvo

Os nomes abaixo são conceituais e podem mudar durante o desenho de contrato.

### Compatibilidade

- `POST /internal/notifications`
- `POST /internal/notification-deliveries/:id/retry`

Esses contratos permanecem durante a transição.

### Inbox e preferências

- `GET /internal/inbox?userId=<id>&instanceId=<id>`
- `POST /internal/inbox/:recipientId/read`
- `POST /internal/inbox/:recipientId/archive`
- `GET /internal/preferences?userId=<id>&instanceId=<id>`
- `PUT /internal/preferences/:preferenceId`

Essas rotas devem ser chamadas por BFF/serviços internos autorizados, não pelo
browser diretamente.

### Comunicação genérica

- `POST /internal/communications/requests`
- `GET /internal/communications/requests/:id`
- `GET /internal/communications/deliveries/:id`

Esse contrato pode virar a superfície nova por cima de notifications, email,
WhatsApp, push e webhook.

### Sender identities e templates

- `GET /internal/sender-domains`
- `POST /internal/sender-domains`
- `GET /internal/sender-domains/:id`
- `PATCH /internal/sender-domains/:id`
- `GET /internal/sender-identities`
- `POST /internal/sender-identities/resolve`
- `POST /internal/sender-identities`
- `GET /internal/sender-identities/:id`
- `PATCH /internal/sender-identities/:id`
- `POST /internal/sender-identities/:id/verify`
- `GET /internal/sender-identities/:id/dns-records`
- `GET /internal/templates`
- `POST /internal/templates/preview`
- `POST /internal/templates`
- `GET /internal/templates/:id`
- `POST /internal/templates/:id/versions`
- `POST /internal/templates/:id/versions/:version/preview`
- `POST /internal/templates/:id/versions/:version/publish`
- `POST /internal/templates/validate`

Status implementado nesta fase: `GET /internal/sender-identities`,
`POST /internal/sender-identities/resolve`, `GET /internal/templates` e
`POST /internal/templates/preview` já existem. Com `DATABASE_URL`, esses
endpoints usam adapters Postgres para identidades e templates; sem Postgres,
usam adapters estático/local.

Também já existe CRUD interno de Postgres para `sender_domains` e
`sender_identities`: `GET/POST/PATCH /internal/sender-domains` e
`GET/POST/PATCH /internal/sender-identities`. Essas rotas retornam
`postgres_not_configured` quando o banco de domínio não está habilitado.
Verificação DNS automática, edição de versões de templates e publicação por API
ainda não foram implementadas.

### Provider accounts e sending pools

- `GET /internal/email-provider-accounts`
- `POST /internal/email-provider-accounts`
- `GET /internal/email-provider-accounts/:id`
- `POST /internal/email-provider-accounts/:id/test`
- `GET /internal/sending-pools`
- `POST /internal/sending-pools`
- `GET /internal/sending-pools/:id`
- `POST /internal/sending-pools/:id/test`

Endpoints de geração por IA não entram na primeira fase. Quando existirem,
devem chamar um serviço/gateway de IA, não providers de modelo diretamente a
partir do Communications.

### Bulk authorization

- `POST /internal/bulk-runs/:campaignRunId/authorize`
- `POST /internal/bulk-runs/:campaignRunId/recipients`
- `POST /internal/bulk-runs/:campaignRunId/start`
- `GET /internal/bulk-runs/:campaignRunId/status`
- `GET /internal/bulk-runs/:campaignRunId/items`
- `POST /internal/bulk-runs/:campaignRunId/status`

O runtime atual implementa executor nativo inicial sobre BullMQ/Postgres para
WhatsApp `chatwoot_silent`. O CRM continua dono da campanha e pode enviar
destinatarios em lote ou expor snapshot paginado para o Communications puxar por
HMAC.

## Eventos alvo

- `communications.delivery.status_changed`
- `communications.message.received`
- `communications.notification.updated`
- `communications.preference.updated`
- `communications.bulk_run.status_changed`
- `communications.unsubscribe.recorded`

Todos os eventos devem carregar `instanceId`, `correlationId` quando existir e
identificadores Ailian estáveis. IDs internos de Frappe ou provider não devem
vazar como contrato público.

Eventos devem sair por um `EventBusPort`. A direção-alvo para pub/sub entre
serviços é NATS. Redis Stream pode continuar como adapter atual/transitório para
eventos já existentes, como `communications.delivery.status_changed`, até a
plataforma consolidar o backbone.

## Persistência alvo

Postgres será o banco de domínio do Communications.

O schema físico ainda deve ser definido, mas os agregados esperados são:

- `communication_requests`;
- `notification_inbox_items`;
- `notification_recipients`;
- `communication_preferences`;
- `email_provider_accounts`;
- `sending_pools`;
- `sender_identities`;
- `sender_domains`;
- `communication_templates`;
- `delivery_records`;
- `delivery_attempts`;
- `suppression_events`;
- `bulk_delivery_runs`;
- `bulk_delivery_items`;
- `bulk_delivery_attempts`.

Redis/BullMQ continuam para fila, locks e estado efêmero.

PocketBase principal fica como adapter/projeção transitória enquanto o front e
outros consumidores migram para contratos do Communications.

### Inbox, realtime e PocketBase

A inbox atual não deve sair do PocketBase por impulso arquitetural. A migração
só deve acontecer quando houver uma resposta explícita para leitura e realtime.

Opções aceitáveis para a transição:

- manter PocketBase como projeção de inbox mesmo com Postgres canônico;
- expor websocket/SSE pelo BFF consumindo eventos do Communications;
- usar polling controlado em rotas internas para casos de menor criticidade;
- criar bridge de eventos `Communications -> EventBus -> BFF/PocketBase`.

Enquanto essa decisão não estiver fechada, notificações comuns continuam no
contrato atual e o Postgres deve entrar primeiro em subdomínios novos, como
sender identities, template registry, delivery ledger ampliado ou bulk runs.

## Fases de evolução

### Fase 0 - Documentação

- ADR da nova fronteira.
- Roadmap conceitual.
- Registro do estado atual como compatibilidade/transição.

Gatilho: agora. Esta fase não altera runtime.

### Fase 1 - Ports/adapters

- Introduzir portas como `NotificationInboxPort`, `PreferencePort`,
  `DeliveryLedgerPort`, `SenderIdentityPort`, `TemplateRegistryPort` e
  `ControlPlanePort`.
- Introduzir `EmailProviderPort` ou `EmailProviderAdapter` sobre o provider
  atual, mantendo Resend como primeira implementação.
- Introduzir `EventBusPort` para desacoplar Redis Stream/NATS.
- Planejar auth serviço-serviço com HMAC sem expor rotas ao browser.
- Manter PocketBase como adapter inicial.
- Preservar `POST /internal/notifications`.

Status no código atual:

- `NotificationInboxPort`, `PreferencePort`, `DeliveryLedgerPort`,
  `ControlPlanePort` e `ContactResolverPort` já isolam o `NotificationsService`
  de chamadas diretas ao PocketBase nesses subdomínios.
- `EventBusPort` já desacopla a publicação de eventos de Redis Stream/NATS.
- `EmailProviderAdapter` já separa o envio técnico do Resend.
- `EmailSendingIdentityPort` já aplica política inicial de remetente/domínio
  com adapter Postgres opcional e fallback atual preservado.
- `EmailTemplateRegistryPort` já isola o catálogo/renderização local de
  templates do provider de email e possui adapter Postgres opcional.
- HMAC serviço-serviço já foi adicionado mantendo Bearer como compatibilidade.

Gatilho: antes de qualquer migração de dado. Esta fase reduz acoplamento sem
mudar o comportamento externo.

### Fase 2 - Postgres foundation

- Adicionar Postgres ao runtime.
- Criar migrações iniciais.
- Persistir novos writes no modelo próprio.
- Avaliar dual-write/projeção para compatibilidade com PocketBase.
- Modelar `sender_identities` e `sender_domains` antes de liberar envios
  customer-visible com domínio de cliente.
- Modelar `email_provider_accounts` e `sending_pools` antes de conectar
  SMTP2GO, Amazon SES, SMTP próprio ou múltiplas contas Resend.
- Modelar `communication_templates` e versões publicadas como base do registry,
  sem geração por IA nessa fase.

Gatilho: quando o primeiro subdomínio novo precisar de estado que não cabe bem
no PocketBase atual, especialmente sender domains, template registry ou ledger
de bulk. Provider accounts e sending pools também entram aqui se a decisão for
conectar SMTP, Amazon SES, SMTP2GO ou múltiplas contas Resend. Não deve ser
usado como justificativa para migrar a inbox inteira antes da estratégia de
realtime.

Status no código atual: `DATABASE_URL` é opcional, o container cria um client
Postgres apenas quando configurado, `/ready` reporta `postgres=not_configured`,
`ok` ou `error`, existe a migração
`migrations/001_communications_domain_foundation.sql` com provider accounts,
sending pools, sender domains, sender identities, templates versionados e bulk
runs agregados, e `migrations/002_seed_local_email_templates.sql` semeia os
templates locais atuais no registry. A migracao
`migrations/003_bulk_delivery_items.sql` adiciona itens e attempts por
destinatario de campanha. O runner `bun run db:migrate` aplica
migrações com controle em `communications.schema_migrations`, checksum e smoke
check via `bun run db:smoke`. O Postgres deve ser dedicado do Communications,
mesmo em stage, para manter isolamento operacional e permitir trocar a
persistência deste serviço sem impactar CRM, Integration ou outros domínios.

### API metadata e i18n

O frontend não deve conhecer enums crus ou detalhes de banco. A direção para as
próximas fases é expor metadados de UI por API interna/BFF: labels, descrições,
erros normalizados, campos obrigatórios, estados permitidos e mensagens i18n por
domínio. Esse contrato deve nascer no Communications e ser consumido pelo BFF do
produto; o browser não chama o Communications diretamente.

### Fase 3 - API de leitura

- Expor inbox e preferências por API interna.
- Conectar Nuxt/BFF a essas APIs.
- Manter browser fora da fronteira direta do Communications.

Gatilho: somente depois de definida a estratégia de realtime/leitura e a
fronteira BFF. Esta fase não deve quebrar o consumo atual do PocketBase.

### Fase 4 - Writeback e replies

- Fortalecer `communications.delivery.status_changed`.
- Fechar round-trip de reply por correlation id.
- Publicar `communications.message.received`.
- Atualizar CRM timeline por eventos, não por leitura direta.
- Publicar unsubscribe, bounce e complaint para o CRM reconciliar consentimento
  comercial.

Gatilho: quando CRM/Frappe precisar refletir status, replies e consentimento
sem consultar providers diretamente.

### Fase 5 - Campanha e bulk

- Integrar CRM/Frappe Campaign/CampaignRun ao Communications.
- Implementar autorização de bulk.
- Implementar executor nativo inicial para WhatsApp `chatwoot_silent`.
- Evoluir executor de bulk: nativo, adapter ou servico especializado conforme
  volume e isolamento operacional.
- Manter Listmonk apenas como possível adapter privado, se for validado. Ele
  não deve ser tratado como fonte canônica nem como dependência estrutural.

Gatilho: quando houver necessidade real de campanha/bulk com domínio do cliente,
isolamento de reputação, consentimento e telemetria agregada. Não é requisito
para notificações comuns.

## Decisões em aberto

- Schema fisico final de Postgres para inbox/preferencias fora do bulk.
- Como migrar dados existentes do PocketBase.
- Estratégia de realtime da inbox depois do PocketBase: projeção, BFF
  websocket/SSE, polling ou bridge de eventos.
- Quais rotas serão publicadas primeiro no OpenAPI.
- Quando separar o executor de bulk nativo em servico especializado.
- Quais providers de email serão suportados primeiro além de Resend.
- Estratégia para SMTP: SMTP2GO, Amazon SES SMTP, SMTP próprio ou outro
  provider dedicado.
- Política de fallback entre providers e quando fallback deve ser proibido por
  risco de reputação, compliance ou duplicidade.
- Modelo de autenticação entre BFF e Communications para leitura de inbox.
- Fluxo de onboarding de domínio: automático via provider, manual por DNS ou
  híbrido assistido pelo suporte.
- Política de isolamento entre domínio/pool transacional e domínio/pool de bulk.
- Processo de provisionamento, escopo, auditoria e rotação de chaves HMAC,
  preferencialmente mantendo compatibilidade com o padrão do Integration
  Service.
- Convenção de subjects/streams NATS e estratégia de migração a partir de Redis
  Stream.
- Contrato futuro com o serviço/gateway de IA para gerar drafts de templates e
  contabilizar consumo de modelos fora do Communications.