email-sender-domains-and-notification-preferences
# Email sender domains and notification preferences
Este documento e o brief de produto/implementacao para a proxima fase de
Communications Service e para o front. A decisao arquitetural esta em
`docs/adr/ADR-002-email-domains-and-notification-preferences.md`; o catalogo
configuravel e o i18n de dominio estao em
`docs/adr/ADR-003-configurable-notification-catalog.md`.
## Objetivo
Permitir que cada tenant envie e-mails customer-visible usando dominio proprio
verificado, sem expor Resend ou credenciais ao usuario final, e transformar
preferencias de notificacao em controles granulares por topico e canal.
## Fronteira
- O browser nao chama o Communications Service diretamente.
- O front deve usar uma API do backend/plataforma com permissao de admin da
instancia.
- O backend/plataforma chama o Communications Service com token interno.
- O Communications Service chama Resend/Chatwoot/outros providers por adapter.
- Providers e chaves nunca aparecem no browser.
## Catalogo configuravel e i18n
O front deve renderizar a tela de notificacoes a partir de um catalogo servido
pelo Communications Service. O front nao deve hardcodar a lista fechada de
topicos, canais ou categorias.
O catalogo e faseado:
- v1: dado estruturado versionado neste repo e servido por endpoint. Topico novo
ainda exige deploy do Communications, mas nao exige deploy de front;
- v2: dado canonico no Control Plane, hoje possivelmente PocketBase via
colecao coordenada fora deste repo. O Communications acessa por port/adapter;
- futuro: admin UI com auditoria para editar topicos, traducoes, defaults e
lifecycle.
O Communications resolve i18n de dominio no servidor. A resposta deve indicar
`requestedLocale`, `resolvedLocale`, `availableLocales` e `fallbackChain`. O
front nao implementa fallback de labels/descricoes de dominio.
Separacao de contratos:
- catalogo: topicos, canais, categorias, textos e defaults;
- capabilities: estado por instancia, como dominio verificado, canal disponivel
e motivo de bloqueio;
- preferences: escolhas por usuario/workspace.
O BFF pode compor esses dados para uma tela unica, mas a origem deve continuar
separada para preservar cache e responsabilidades.
## UX - Dominios de e-mail
Tela recomendada:
- `Configuracoes > Comunicacoes > Dominios de e-mail`
Estados principais:
- sem dominio configurado;
- dominio criado, aguardando DNS;
- verificacao em andamento;
- dominio verificado;
- dominio com falha;
- dominio desativado.
Fluxo de adicionar dominio:
1. usuario informa o dominio/subdominio de envio;
2. UI recomenda subdominio, como `send.seudominio.com` ou
`notifications.seudominio.com`, em vez do dominio raiz;
3. backend cria o dominio no Communications Service;
4. Communications Service cria o dominio no provider e retorna registros DNS;
5. UI mostra registros em tabela com botoes de copiar;
6. usuario adiciona os registros no DNS;
7. usuario clica em "Verificar agora";
8. UI acompanha `pending_dns`, `verifying`, `verified` ou `failed`;
9. depois de `verified`, usuario cria identidades de remetente.
Tabela de registros DNS:
- tipo;
- host/nome;
- valor;
- prioridade, quando existir;
- TTL, quando existir;
- status;
- botao copiar host;
- botao copiar valor;
- dica de DNS provider quando conhecida.
Campos de identidade de remetente:
- nome exibido;
- e-mail de remetente;
- e-mail de resposta;
- dominio associado;
- padrao da instancia;
- ativo/inativo.
Regras de UX:
- envio customer-visible fica bloqueado ate existir dominio `verified` e uma
identidade ativa;
- e-mails da propria plataforma Ailian continuam usando dominio Ailian;
- nao mostrar "Resend" como conceito primario para o cliente;
- detalhes de provider podem ficar em modo suporte/admin;
- status de falha deve explicar proximo passo sem culpar o usuario por termos
tecnicos;
- toda tela deve deixar claro que registros DNS podem levar tempo para propagar.
Copys sugeridos:
- "Configure um dominio para enviar e-mails aos seus clientes."
- "E-mails operacionais da Ailian continuam usando dominio Ailian."
- "Para proteger a reputacao do seu dominio principal, recomendamos usar um subdominio."
- "Nao foi possivel enviar: configure um dominio verificado."
- "A verificacao pode levar alguns minutos e, em alguns provedores DNS, ate 72 horas."
## UX - Preferencias granulares
Tela recomendada:
- `Configuracoes > Notificacoes`
Abas recomendadas:
- `Minhas notificacoes`;
- `Padroes do workspace`;
- `Catalogo de eventos`, somente admin/suporte se necessario.
Formato recomendado:
- linhas por topico/evento;
- colunas por canal: in-app, toast/realtime, push, e-mail, WhatsApp;
- celula com controle explicito: `toggle`, `locked`, `optin` ou `frequency`;
- celula com disponibilidade explicita: `allowed`, `topic_not_allowed`,
`instance_unavailable` ou `mandatory`;
- estado herdado vs override explicito;
- cadeado para eventos obrigatorios;
- indicacao de canal indisponivel com motivo vindo de capabilities;
- filtros por produto, prioridade e categoria.
Topicos iniciais sugeridos:
- `crm.email.reply_received`;
- `crm.email.delivery_failed`;
- `crm.sla.due_soon`;
- `crm.sla.overdue`;
- `automation.run_failed`;
- `billing.invoice_issued`;
- `connectai.lead_handoff`.
Copys sugeridos:
- "Escolha como voce quer ser avisado para cada tipo de evento."
- "Obrigatorio: esta notificacao nao pode ser desativada."
- "Herdado do workspace."
- "Personalizado por voce."
- "WhatsApp exige opt-in explicito."
## Contratos internos propostos
Os endpoints abaixo sao proposta de implementacao futura. Eles nao estao
implementados nesta branch.
```text
GET /internal/email/sending-domains?instanceId=<id>
POST /internal/email/sending-domains
GET /internal/email/sending-domains/:id
POST /internal/email/sending-domains/:id/verify
POST /internal/email/sending-domains/:id/disable
GET /internal/email/sender-identities?instanceId=<id>
POST /internal/email/sender-identities
PATCH /internal/email/sender-identities/:id
GET /internal/notification-catalog?locale=<locale>
GET /internal/notification-capabilities?instanceId=<id>&locale=<locale>
GET /internal/notification-preferences?instanceId=<id>&userId=<id>
PUT /internal/notification-preferences
```
Exemplo de dominio retornado:
```json
{
"id": "dom_123",
"instanceId": "inst_123",
"domain": "send.example.com",
"status": "pending_dns",
"capabilities": {
"sending": true,
"receiving": false
},
"records": [
{
"kind": "SPF",
"type": "TXT",
"host": "send",
"value": "\"v=spf1 include:amazonses.com ~all\"",
"status": "pending"
}
]
}
```
Exemplo de identidade:
```json
{
"id": "sender_123",
"instanceId": "inst_123",
"domainId": "dom_123",
"fromName": "Equipe Example",
"fromEmail": "[email protected]",
"replyTo": "[email protected]",
"isDefault": true,
"status": "active"
}
```
Exemplo de topico:
```json
{
"schemaVersion": "notification-catalog.v1",
"key": "crm.sla.overdue",
"lifecycleStatus": "active",
"categoryKey": "crm",
"display": {
"label": "SLA vencido",
"description": "Aviso quando uma oportunidade ou lead passa do prazo de resposta."
},
"priority": "high",
"mandatory": false,
"allowedChannels": ["in_app", "realtime", "push", "email"],
"defaultChannels": ["in_app", "push", "email"],
"channelRules": [
{
"channelKey": "email",
"control": "toggle",
"availability": "allowed",
"defaultValue": true
}
]
}
```
Regras de `topicKey`:
- `topicKey` e contrato entre produtores e Communications, nao apenas UI;
- depois de ativo, nao renomeie a chave; altere label/descricao ou crie uma
chave nova;
- topico desconhecido, `draft` ou `disabled` deve ser rejeitado na ingestao
canonica antes de criar deliveries;
- topico `deprecated` pode ser aceito durante janela de transicao e deve emitir
metrica/aviso.
Exemplo de preferencia:
```json
{
"instanceId": "inst_123",
"userId": "usr_123",
"topicKey": "crm.sla.overdue",
"channels": {
"in_app": true,
"realtime": true,
"push": true,
"email": false,
"whatsapp": false
}
}
```
## Dispatch
Antes de enviar e-mail customer-visible, o worker deve verificar:
- instancia do envio;
- dominio de envio verificado;
- identidade de remetente ativa;
- bloqueios de billing/entitlement quando aplicavel;
- supressoes tecnicas/provider-side;
- preferencias do destinatario, quando houver usuario interno;
- politica de opt-in do canal.
Se faltar dominio ou identidade, o envio nao deve trocar para dominio Ailian. O
resultado esperado e bloqueio/falha de configuracao visivel para o produto
chamador.
## Compatibilidade com o modelo atual
O modelo atual de `notification_preferences` pode continuar existindo durante a
migracao:
- flags por canal viram defaults globais;
- a matriz granular sobrescreve esses defaults quando existir;
- WhatsApp continua exigindo opt-in positivo;
- eventos obrigatorios continuam podendo usar `bypassPreferences`, mas devem ser
restritos a casos legais, seguranca ou operacao essencial.
## Sequencia recomendada
1. Persistir dominios de envio e identidades de remetente.
2. Adicionar adapter de provider para criar/verificar dominios.
3. Expor APIs internas de dominio e identidade.
4. Fazer dispatch bloquear e-mail customer-visible sem dominio verificado.
5. Criar catalogo v1 como seed/dado estruturado no repo.
6. Expor catalogo, capabilities e preferencias em contratos separados.
7. Adicionar preferencias granulares e resolver fallback para flags legadas.
8. Integrar front por backend/proxy autorizado.
9. Evoluir catalogo v2 para Control Plane/PocketBase coordenado.
## Criterios de aceite
- cliente consegue cadastrar subdominio e copiar registros DNS;
- status de verificacao e atualizado sem expor provider como produto;
- e-mail customer-visible nao usa dominio Ailian em producao;
- identidade de remetente so fica ativa em dominio verificado;
- front mostra bloqueio claro quando e-mail externo nao esta configurado;
- preferencias permitem ligar/desligar canal por topico;
- eventos obrigatorios aparecem bloqueados na UI;
- topico novo nao exige deploy de front;
- topico desconhecido nao gera entrega silenciosa;
- Communications Service continua sendo o unico ponto de dispatch.