notification-flow
# Notification Flow
## Objetivo
Explicar o fluxo canonico de criacao e execucao de notificacoes no Communications Service.
## Contrato recomendado
Quem produz notificacoes nao deve escrever diretamente em:
- `notifications`
- `notification_recipients`
- `notification_deliveries`
O contrato recomendado agora e:
1. um produtor confiavel, como `n8n`, chama `POST /internal/notifications`
2. o Communications Service valida o payload
3. aplica `notification_preferences`
4. cria `notifications`
5. cria `notification_recipients`
6. cria `notification_deliveries`
7. enfileira imediatamente o que ja estiver elegivel
## Navegacao
`navigationKey` passa a ser a identidade estavel de navegacao da notificacao.
Regras:
- o produtor pode enviar `navigationKey`
- se nao enviar, o backend gera automaticamente
- `route` deixa de ser a origem da navegacao
- o backend sempre persiste `route` como:
- `/dashboard/notifications/r/<navigationKey>`
- se `payload.ui.copyOnOpen` vier com:
- `message`
- `body`
- `phone`
o backend anexa `?copy=<valor>` na rota derivada
Na pratica:
- `navigationKey` identifica a notificacao no front
- `route` vira apenas a URL resolvedora persistida
- `dedupeKey` continua sendo so deduplicacao
- `groupKey` continua sendo so agrupamento
- para WhatsApp, envie `payload.links.whatsAppShareUrl`
- para os botoes do push, use `actions` com `action` e `title`
Exemplo de lead handoff com WhatsApp:
```json
{
"actions": [
{ "action": "copy-handoff", "title": "Copiar mensagem" },
{ "action": "open-whatsapp", "title": "Abrir WhatsApp" },
{ "action": "open-sheet", "title": "Abrir planilha" }
],
"payload": {
"kind": "lead_handoff",
"ui": {
"copyOnOpen": "body"
},
"links": {
"whatsAppShareUrl": "https://wa.me/5511999999999?text=...",
"sheetUrl": "https://docs.google.com/spreadsheets/d/..."
},
"push": {
"body": "Novo lead de Taboao da Serra - SP"
}
}
}
```
O front continua consumindo o PocketBase como antes.
## Quem chama essa API
Chamadores esperados:
- `n8n`
- backend interno
- workflows trusted
- servicos internos
Nao e uma API pensada para o browser chamar diretamente, porque ela exige:
- `Authorization: Bearer <SERVICE_AUTH_TOKEN>`
## O que a API faz
Quando recebe uma requisicao valida, o servico:
- trata a criacao como uma unica operacao
- usa `dedupeKey` ou `idempotencyKey` quando enviados
- cria a notificacao inicialmente como `draft`
- cria recipients e deliveries necessarios
- faz compensacao em caso de falha durante a montagem
- muda a notificacao para `queued` quando houver deliveries
- muda a notificacao para `cancelled` quando tudo for suprimido por preferencia
Na pratica, isso elimina a necessidade de o consumidor conhecer 3 tabelas.
## Audiencia e scopes
O servico suporta os 3 scopes persistidos em `notifications`:
- `user`: notifica usuarios explicitos em `recipients`
- `instance`: notifica usuarios de uma instancia, com opcao de filtrar por role
- `application`: notifica usuarios ligados a instancias na plataforma, com opcao de filtrar por role
Mesmo para `scope=instance` e `scope=application`, o Communications Service cria `notification_recipients` para cada usuario resolvido. Isso e necessario porque as regras de leitura do PocketBase dependem de `notification_recipients`.
Para resolver audiencia automaticamente, use `audience.roles`:
```json
{
"scope": "instance",
"instanceId": "4f0kfmefs6x09lo",
"audience": {
"roles": ["super_admin", "gestor", "notificacoes"]
}
}
```
Regras:
- se `recipients` vier preenchido, os usuarios explicitos sao mantidos
- se `audience` vier junto, usuarios resolvidos sao mesclados e duplicados sao removidos
- `scope=instance` exige `instanceId` e busca em `user_instance_roles.instance`
- `scope=application` busca em `user_instance_roles` sem filtrar instancia
- `scope=user` continua exigindo `recipients`
## Como `notification_preferences` entra no fluxo
`notification_preferences` e interpretado no proprio Communications Service, nao no front.
Em ambientes com os hooks externos do PocketBase habilitados, preferencias basicas sao provisionadas automaticamente quando usuarios e vinculos em instancias sao criados. Esse provisionamento vive fora deste repositorio e esta documentado em:
- `docs/concepts/pocketbase-notification-preferences-hooks.md`
Regras suportadas na ingestao:
- `in_app_enabled` controla a disponibilidade do item na experiencia in-app
- `toast_enable` controla especificamente o canal `realtime` usado para toast/sinalizacao imediata
- `push_enabled` controla o canal `push`
- `email_enabled` controla o canal `email`
- `whatsapp_enabled` controla o canal `whatsapp`
- `mute_low_priority` suprime notificacoes `low`
- `require_action_push` permite push apenas quando `requireAction = true`
- `mute_until` suprime envios ate a data configurada
O que ainda nao esta sendo interpretado na ingestao:
- `quiet_hours`
Esse ponto continua documentado como evolucao futura.
Observacao: para seguranca operacional, `whatsapp` exige `whatsapp_enabled=true`. Quando nao ha preferencia provisionada para o usuario/escopo, o canal `whatsapp` e suprimido.
## Trigger operacional
O servico tem 2 modos de disparo:
### 1. Imediato
Ao criar pela API interna, deliveries sem agendamento futuro ja sao enfileiradas na hora.
### 2. Scan periodico
Mesmo sem trigger imediato, os workers continuam consultando o PocketBase em intervalo fixo.
Padrao atual:
- `NOTIFICATION_SCAN_EVERY_MS = 30000`
Ou seja:
- ate 30 segundos para deliveries `queued`
- retries respeitam `next_retry_at`
## Fluxo resumido
1. `n8n` chama `POST /internal/notifications`
2. o servico persiste tudo no PocketBase
3. o servico ja enfileira deliveries elegiveis
4. workers processam por canal/provider
5. `notification_deliveries`, `notification_recipients` e `notifications` sao atualizadas
## Providers externos
### Email com Resend
Para usar Resend, envie `channel=email` e `provider=resend`.
Quando `RESEND_API_KEY` estiver configurado, `provider=resend` e assumido automaticamente para deliveries criadas pela API canonica com `channel=email`.
O provider tenta resolver o email pelo usuario do PocketBase usando `PB_USER_EMAIL_FIELD` e fallback para `email`. O payload pode sobrescrever o destino com `payload.email.to`.
Exemplo:
```json
{
"channels": ["email"],
"providers": {
"email": "resend"
},
"payload": {
"email": {
"from": "Ailian <[email protected]>",
"subject": "Novo lead recebido",
"html": "<p>Um novo lead chegou.</p>"
}
}
}
```
Para templates de email versionados neste repositorio, use `payload.email.template`.
O Communications Service renderiza `subject`, `html` e `text` localmente antes
de chamar o Resend. Não use `templateId` para esses templates.
Templates disponiveis:
- `payment-approved`: confirmação de pagamento aprovado
- `invoice-issued`: aviso de nota fiscal emitida
Exemplo de pagamento aprovado:
```json
{
"channels": ["email"],
"providers": {
"email": "resend"
},
"payload": {
"email": {
"template": "payment-approved",
"subject": "Pagamento aprovado",
"variables": {
"appName": "Ailian",
"customerName": "Marina Costa",
"orderId": "PED-2026-0001",
"productName": "Ghostwriter Starter",
"paymentAmount": "R$ 199,90",
"paymentMethod": "Cartao de credito",
"paidAt": "11/05/2026 18:30",
"appUrl": "https://app.ailian.com.br/billing"
}
}
}
}
```
Exemplo de nota fiscal emitida:
```json
{
"channels": ["email"],
"providers": {
"email": "resend"
},
"payload": {
"email": {
"template": "invoice-issued",
"subject": "Nota fiscal emitida",
"variables": {
"appName": "Ailian",
"customerName": "Marina Costa",
"invoiceNumber": "NF-e 1024",
"productName": "Creditos Ailian 20K",
"invoiceAmount": "R$ 199,90",
"issuedAt": "11/05/2026 18:45",
"invoiceUrl": "https://app.ailian.com.br/invoices/nf-001",
"appUrl": "https://app.ailian.com.br/billing"
}
}
}
}
```
A lista atual tambem fica disponivel em:
- `/docs/email-templates`
- `/docs/email-templates.json`
### WhatsApp com Chatwoot
Para usar WhatsApp em notifications, envie `channel=whatsapp` e `provider=chatwoot`.
`provider=chatwoot` e assumido automaticamente para deliveries criadas pela API canonica com `channel=whatsapp`.
Regras:
- `whatsapp_enabled` precisa estar `true` na preferencia escolhida do usuario
- sem preferencia provisionada, `whatsapp` e suprimido
- o provider tenta resolver o telefone em `profiles` usando `PB_PROFILE_CALLING_CODE_FIELD` + `PB_PROFILE_PHONE_FIELD`
- se nao houver profile/telefone, tenta fallback no usuario do PocketBase usando `PB_USER_WHATSAPP_FIELD`
- tambem aceita override por `payload.whatsapp.phoneNumber` ou `payload.targets.whatsapp.phoneNumber`
Exemplo com template WhatsApp:
```json
{
"channels": ["realtime", "push", "whatsapp"],
"providers": {
"push": "webpush",
"whatsapp": "chatwoot"
},
"payload": {
"whatsapp": {
"templateName": "novo_lead_recebido",
"language": "pt_BR",
"processedParams": {
"lead_name": "Fulano",
"city": "Sao Paulo"
}
}
}
}
```
Se ja existir uma conversa ou contato no Chatwoot, o payload pode informar:
```json
{
"payload": {
"whatsapp": {
"conversationId": 123,
"contactId": 456
}
}
}
```
## Exemplo real usado nas docs
Os exemplos interativos do Scalar usam hoje:
- `instanceId = 4f0kfmefs6x09lo`
- `userId = y1412767403q7jx`
Services usados nos exemplos:
- `z97bsc1rlv9qbk3` para chatbot/ConectAi
- `51x9x66y6z86h8w` para prospeccao ativa
- `x4q5cd5zj5qtwi3` para geracao de conteudo