24 KiB
Sistema de Lembretes Automáticos — WhatsApp, E-mail, SMS
Agência PSI — Arquitetura completa Data: 2026-03-21 Autor: Leonardo Nohama
Sumário
- Decisão de Provedor WhatsApp
- Modelagem do Banco de Dados
- Lógica de Agendamento (Cron + Edge Functions)
- Integração Evolution API (MVP)
- Integração API Oficial Meta (Escala)
- Frontend: Telas de Configuração
- LGPD e Boas Práticas
- Diagrama do Fluxo Completo
- Checklist de Produção
1. Decisão de Provedor WhatsApp
Comparativo de Provedores
| Critério | Evolution API | WPPConnect | Z-API (grátis) | Z-API Pro | Twilio (Meta oficial) | 360dialog (Meta oficial) | Zenvia (Meta oficial) |
|---|---|---|---|---|---|---|---|
| Tipo | Não-oficial (baileys) | Não-oficial | Não-oficial | Não-oficial (infra deles) | API Oficial Meta | API Oficial Meta | API Oficial Meta |
| Custo mensal | R$ 0 (self-hosted) | R$ 0 (self-hosted) | R$ 0 (limite de msgs) | R$ 99–299/mês | ~R$ 0.30/msg (conversa) | €49/mês + msg | R$ 0.15–0.40/msg |
| Setup | 2–4h (Docker) | 4–8h | 30min (SaaS) | 1h (SaaS) | 1–2 semanas (aprovação) | 3–5 dias | 3–5 dias |
| Risco de ban | MÉDIO-ALTO | ALTO | MÉDIO | MÉDIO | ZERO | ZERO | ZERO |
| Templates Meta | ❌ Não suporta | ❌ | ❌ | ❌ | ✅ Obrigatório | ✅ Obrigatório | ✅ Obrigatório |
| Webhooks status | ✅ Completo | ✅ Parcial | ✅ | ✅ | ✅ Completo | ✅ Completo | ✅ Completo |
| Multi-instância | ✅ Nativo | ❌ Manual | ❌ | ✅ | ✅ Via WABA | ✅ | ✅ |
| Uptime SLA | Depende de você | Depende de você | 99.5% | 99.5% | 99.95% | 99.9% | 99.9% |
| Escalabilidade | ~500 msgs/dia seguro | ~300 msgs/dia | ~200 msgs/dia | ~2000 msgs/dia | Ilimitado | Ilimitado | Ilimitado |
Recomendação
MVP (0–100 clínicas): Evolution API self-hosted
- Custo zero, setup rápido, suficiente para validar o produto
- Cada terapeuta conecta seu próprio número via QR Code
- Limite prático: ~500 mensagens/dia por número
- Mitigação de ban: mensagens personalizadas (não genéricas), intervalos entre envios, máximo 2 lembretes por sessão
Escala (100+ clínicas): Migrar para API Oficial da Meta via 360dialog ou Twilio
- Zero risco de banimento
- Templates aprovados pela Meta = entrega garantida
- Custo previsível por conversa (~R$ 0.25–0.40 por conversa de 24h)
- Suporte a botões interativos (confirmar/cancelar)
Estratégia de migração: O sistema será projetado com abstração de provedor desde o início.
A tabela notification_channels registra qual provedor cada tenant usa. Trocar de Evolution
para Meta oficial = mudar o provider e credenciais, sem alterar a fila ou templates.
2. Modelagem do Banco de Dados
Nota de integração: O sistema existente já possui:
email_templates_global/email_templates_tenant→ serão estendidos (não duplicados)notifications→ continuam para notificações in-app (realtime)profiles.notify_reminders→ será respeitado como opt-out globalTEMPLATE_CHANNELSem emailTemplateConstants.js → já prevê whatsapp/sms
Relação entre tabelas novas e existentes
┌─────────────────────────────────────────────────────────────────┐
│ EXISTENTES (não alterar) │
│ │
│ email_templates_global ──→ templates de email (11 seeds) │
│ email_templates_tenant ──→ overrides por tenant/owner │
│ notifications ──→ notificações in-app (realtime) │
│ profiles ──→ notify_reminders, notify_system_email│
│ agenda_eventos ──→ sessões com patient_id, inicio_em │
│ patients ──→ nome_completo, telefone, email │
├─────────────────────────────────────────────────────────────────┤
│ NOVAS TABELAS │
│ │
│ notification_channels ──→ config WhatsApp/SMS por tenant │
│ notification_templates ──→ templates multi-canal (wpp/sms) │
│ notification_queue ──→ fila de envio │
│ notification_logs ──→ histórico completo │
│ notification_preferences ──→ opt-in/opt-out por paciente │
│ notification_schedules ──→ regras de quando disparar │
└─────────────────────────────────────────────────────────────────┘
3. Lógica de Agendamento
Fluxo Completo
pg_cron (a cada 5 min)
│
├──→ populate_notification_queue() ← PL/pgSQL function
│ Busca agenda_eventos com inicio_em futuro,
│ cruza com notification_schedules ativas,
│ verifica notification_preferences do paciente,
│ insere na notification_queue com idempotency_key
│
└──→ HTTP call → Edge Function: process-notification-queue
│
├── Busca itens pendentes (status = 'pendente', scheduled_at <= now())
├── Marca como 'processando' (lock otimista via updated_at)
├── Resolve variáveis do template
├── Despacha para o provedor correto:
│ ├── WhatsApp → Evolution API ou Meta API
│ ├── Email → Resend / SendGrid / SMTP
│ └── SMS → Zenvia / Twilio
├── Atualiza status → 'enviado' ou 'falhou'
├── Insere em notification_logs
└── Em caso de falha: agenda retry exponencial
Retry Exponencial
Tentativa 1: imediato
Tentativa 2: +5 minutos
Tentativa 3: +15 minutos
Tentativa 4: +60 minutos
Tentativa 5: +4 horas
Máximo: 5 tentativas → marca como 'falhou' definitivamente
Prevenção de Duplicatas
- Idempotency key =
{agenda_evento_id}:{schedule_key}:{canal}:{data_sessao} - UNIQUE constraint na notification_queue sobre idempotency_key
- Lock otimista no processamento:
UPDATE ... WHERE status = 'pendente' AND updated_at = ? - pg_cron não overlap: usa
pg_try_advisory_lock()no populate
4. Integração Evolution API (MVP)
Setup Docker
# docker-compose.evolution.yml
version: '3.8'
services:
evolution-api:
image: atendai/evolution-api:latest
ports:
- "8080:8080"
environment:
- AUTHENTICATION_API_KEY=sua_chave_global_aqui
- DATABASE_PROVIDER=postgresql
- DATABASE_CONNECTION_URI=postgresql://user:pass@host:5432/evolution
- WEBHOOK_GLOBAL_URL=https://seu-dominio.com/api/webhooks/evolution
- WEBHOOK_GLOBAL_ENABLED=true
- WEBHOOK_EVENTS_STATUS_INSTANCE=true
- WEBHOOK_EVENTS_MESSAGES_UPSERT=true
- WEBHOOK_EVENTS_SEND_MESSAGE=true
volumes:
- evolution_data:/evolution/store
restart: unless-stopped
volumes:
evolution_data:
Endpoints Necessários
Base URL: https://evolution.seudominio.com
POST /instance/create → criar instância para o tenant
GET /instance/connect/{name} → obter QR Code para conectar número
GET /instance/connectionState/{name} → verificar status da conexão
POST /message/sendText/{name} → enviar mensagem de texto
POST /message/sendMedia/{name} → enviar com mídia (opcional)
DELETE /instance/delete/{name} → remover instância
Payload de Envio
// POST /message/sendText/{instance_name}
{
"number": "5516999887766",
"text": "Olá Ana Clara! 👋\n\nLembrete: você tem sessão amanhã, 21/03, às 14:00 com Dra. Beatriz Costa.\n\n📍 Online via Google Meet\n🔗 https://meet.google.com/abc-defg-hij\n\nPara confirmar, responda OK.\nPara cancelar, responda CANCELAR.\n\nAgência PSI"
}
Webhook de Status
// POST /api/webhooks/evolution (recebido do Evolution)
{
"event": "messages.update",
"instance": "clinica_abc",
"data": {
"key": {
"remoteJid": "5516999887766@s.whatsapp.net",
"id": "3EB0A0B6F..."
},
"update": {
"status": 3 // 1=pendente, 2=enviado ao servidor, 3=entregue, 4=lido
}
}
}
Credenciais no Supabase
Armazenadas na tabela notification_channels.credentials como JSONB criptografado:
{
"api_url": "https://evolution.seudominio.com",
"api_key": "chave_global_evolution",
"instance_name": "clinica_dr_beatriz",
"connected_number": "5516999887766",
"connection_status": "open"
}
A criptografia das credenciais usa
pgcryptocom chave armazenada como variável de ambiente do Supabase (Vault). Veja a função SQLencrypt_credentials().
5. Integração API Oficial Meta
Template de Lembrete para Aprovação
Nome: session_reminder_v1
Categoria: UTILITY
Idioma: pt_BR
HEADER: 📋 Lembrete de Sessão
BODY: Olá {{1}}! Sua sessão com {{2}} está agendada para {{3}} às {{4}}.
Modalidade: {{5}}
BUTTONS:
[quick_reply] ✅ Confirmar presença
[quick_reply] ❌ Preciso cancelar
FOOTER: Agência PSI — Tecnologia aplicada à escuta
Envio via Graph API
POST https://graph.facebook.com/v19.0/{phone_number_id}/messages
Authorization: Bearer {access_token}
Content-Type: application/json
{
"messaging_product": "whatsapp",
"to": "5516999887766",
"type": "template",
"template": {
"name": "session_reminder_v1",
"language": { "code": "pt_BR" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Ana Clara" },
{ "type": "text", "text": "Dra. Beatriz Costa" },
{ "type": "text", "text": "21/03/2026" },
{ "type": "text", "text": "14:00" },
{ "type": "text", "text": "Online" }
]
}
]
}
}
Webhook de Status (Meta)
// POST /api/webhooks/meta-whatsapp
{
"entry": [{
"changes": [{
"value": {
"statuses": [{
"id": "wamid.HBgN...",
"status": "delivered", // sent, delivered, read, failed
"timestamp": "1711036800",
"recipient_id": "5516999887766",
"errors": []
}]
}
}]
}]
}
6. Frontend: Telas de Configuração
6.1 Configuração de Canal (ConfiguracoesCanaisPage.vue)
┌─────────────────────────────────────────────────┐
│ 📡 Canais de Notificação │
├─────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 💬 WhatsApp [Ativo ✅] │ │
│ │ Provedor: Evolution API │ │
│ │ Número: +55 16 99988-7766 │ │
│ │ Status: 🟢 Conectado │ │
│ │ │ │
│ │ [Reconectar] [Ver QR Code] [Testar] │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 📧 E-mail [Ativo ✅] │ │
│ │ Provedor: Resend │ │
│ │ Remetente: clinica@drbeat... │ │
│ │ Status: 🟢 Verificado │ │
│ │ │ │
│ │ [Configurar SMTP] [Testar] │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 📱 SMS [Inativo ⬜] │ │
│ │ Não configurado │ │
│ │ │ │
│ │ [Ativar] │ │
│ └──────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
6.2 Templates (ConfiguracoesNotifTemplatesPage.vue)
- Lista de templates por domínio (Sessão, Triagem, Sistema)
- Preview ao vivo com variáveis mock (já existe no sistema)
- Cada template pode ser personalizado por canal (email / whatsapp / sms)
- Herança: se o tenant não customizou, mostra o template global com badge "Padrão"
6.3 Regras de Envio (ConfiguracoesNotifRegrasPage.vue)
┌──────────────────────────────────────────────┐
│ ⏰ Regras de Envio de Lembretes │
├──────────────────────────────────────────────┤
│ │
│ Lembrete de Sessão │
│ ├── 24 horas antes [WhatsApp ✅] [Email ✅] │
│ ├── 2 horas antes [WhatsApp ✅] [Email ⬜] │
│ └── 30 min antes [WhatsApp ⬜] [Email ⬜] │
│ │
│ Confirmação de Sessão │
│ ├── Imediata (ao criar) [Email ✅] │
│ └── Imediata [WhatsApp ✅] │
│ │
│ Cancelamento │
│ └── Imediata [WhatsApp ✅] [Email ✅]│
│ │
│ Boas-vindas (novo paciente) │
│ └── Imediata [WhatsApp ✅] [Email ✅]│
│ │
│ ⚙️ Horário permitido: 08:00 – 20:00 │
│ 📅 Não enviar: Domingos e feriados │
│ │
│ [Salvar configurações] │
└──────────────────────────────────────────────┘
6.4 Logs de Envio (ConfiguracoesNotifLogsPage.vue)
- Tabela paginada com filtros por canal, status, período
- Detalhes por envio: template usado, variáveis resolvidas, resposta do provedor
- Estatísticas: total enviado, entregue, lido, falhou (últimos 30 dias)
- Export CSV
6.5 Opt-out pelo Paciente
- WhatsApp: Paciente responde "SAIR" → webhook captura → atualiza
notification_preferences - Email: Link "Cancelar inscrição" no rodapé → página pública de opt-out
- Portal do paciente (futuro): Toggle na área logada do paciente
7. LGPD e Boas Práticas
Coleta de Consentimento
- Cadastro externo (CadastroPacienteExterno.vue): já tem checkbox LGPD
- Cadastro pelo terapeuta: adicionar checkbox "Paciente autoriza receber lembretes por WhatsApp/E-mail"
- Primeiro lembrete: incluir mensagem "Responda SAIR a qualquer momento para parar de receber mensagens"
Armazenamento Seguro
- Telefone do paciente: armazenado na tabela
patients.telefone(já existe) - Credenciais do provedor:
notification_channels.credentialscriptografado compgcrypto - Chave de criptografia: Supabase Vault (variável de ambiente, nunca no código)
Retenção de Logs
notification_logs: reter por 2 anos (exigência legal para comprovação de comunicação)notification_queue: limpar itens processados após 90 dias (via pg_cron)notification_preferences: manter enquanto o paciente estiver ativo
Opt-out Imediato
- Resposta "SAIR" no WhatsApp → webhook →
notification_preferences.whatsapp_opt_in = false - Efeito imediato: todas as mensagens pendentes na fila para aquele paciente são canceladas
- Trigger SQL: ao atualizar opt-out, cancela itens pendentes na queue
8. Diagrama do Fluxo Completo
┌──────────────┐
│ agenda_eventos│ ← terapeuta cria/edita sessão
└──────┬───────┘
│
▼
┌──────────────────────────────────────┐
│ pg_cron: populate_notification_queue │ ← roda a cada 5 min
│ │
│ 1. Busca sessões com inicio_em │
│ entre agora e +48h │
│ 2. Cruza com notification_schedules │
│ (ex: 24h antes, 2h antes) │
│ 3. Verifica: │
│ - notification_preferences (opt-in)│
│ - notification_channels (canal ativo)│
│ - profiles.notify_reminders │
│ - agenda_eventos.status ≠ cancelado│
│ 4. Gera idempotency_key │
│ 5. INSERT INTO notification_queue │
│ ON CONFLICT DO NOTHING │
└──────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ pg_cron: chama Edge Function │ ← roda a cada 5 min (offset 2min)
│ POST /functions/v1/process-notif-queue│
└──────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Edge Function: process-notif-queue │
│ │
│ 1. SELECT ... FROM notification_queue│
│ WHERE status = 'pendente' │
│ AND scheduled_at <= now() │
│ LIMIT 50 │
│ FOR UPDATE SKIP LOCKED │
│ │
│ 2. Para cada item: │
│ a. Marca 'processando' │
│ b. Resolve template + variáveis │
│ c. Despacha para provedor: │
│ ┌──────────────────────┐ │
│ │ channel = 'whatsapp' │ │
│ │ → Evolution API │ │
│ │ ou Meta Graph API │ │
│ ├──────────────────────┤ │
│ │ channel = 'email' │ │
│ │ → Resend / SMTP │ │
│ ├──────────────────────┤ │
│ │ channel = 'sms' │ │
│ │ → Zenvia / Twilio │ │
│ └──────────────────────┘ │
│ d. Atualiza status │
│ e. INSERT notification_logs │
│ │
│ 3. Itens com falha: │
│ attempts += 1 │
│ next_retry_at = exponential │
│ status = attempts >= 5 │
│ ? 'falhou' : 'pendente' │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Webhook Handler │
│ POST /functions/v1/notif-webhook │
│ │
│ Recebe status do provedor: │
│ - Evolution: messages.update │
│ - Meta: webhook de status │
│ - Email: bounce/delivery events │
│ │
│ Atualiza notification_logs: │
│ - delivered_at, read_at, failed_at │
│ - provider_status, provider_response │
│ │
│ Se resposta = "SAIR": │
│ → UPDATE notification_preferences │
│ SET whatsapp_opt_in = false │
│ → CANCEL pendentes na queue │
└──────────────────────────────────────┘
9. Checklist de Produção
Infraestrutura
- Subir Evolution API (Docker) em VPS dedicada
- Configurar domínio + SSL para Evolution API
- Configurar Supabase Vault com chave de criptografia
- Habilitar extensão
pgcryptono Supabase - Habilitar extensão
pg_cronno Supabase - Configurar DNS para webhook (ex:
webhooks.agenciapsi.com.br)
Banco de Dados
- Rodar migration: tabelas de notificação
- Rodar migration: functions PL/pgSQL (populate queue, encrypt/decrypt)
- Configurar pg_cron jobs (populate + process)
- Verificar RLS policies em todas as tabelas
- Seed: notification_schedules padrão (24h, 2h)
- Seed: notification_templates padrão (whatsapp + sms)
Edge Functions
- Deploy:
process-notif-queue - Deploy:
notif-webhook - Configurar secrets:
EVOLUTION_API_KEY,ENCRYPTION_KEY - Testar com payload simulado
Frontend
- Tela de configuração de canais
- Tela de templates (estender ConfiguracoesEmailTemplatesPage existente)
- Tela de regras de envio
- Tela de logs
- Adicionar consentimento no cadastro de paciente
- Adicionar opt-out no rodapé de emails
Testes
- Teste E2E: criar sessão → verificar queue populada → verificar envio
- Teste: opt-out WhatsApp → verificar cancelamento na queue
- Teste: retry após falha → verificar exponential backoff
- Teste: idempotency → rodar populate 2x → verificar sem duplicatas
- Teste: tenant isolation → verificar RLS
- Teste de carga: 1000 mensagens na queue → medir throughput
Monitoramento
- Alerta se queue > 500 itens pendentes
- Alerta se taxa de falha > 10% em 1h
- Dashboard de métricas (envios/dia, taxa de entrega, tempo médio de processamento)
- Log de erros no Supabase Logs
Documento gerado como parte da arquitetura do sistema Agência PSI. As implementações SQL e JavaScript estão nos arquivos separados referenciados abaixo.