Webhooks e eventos
A tabela events (ULID), como uma mudança de estado vira evento, os webhooks que chegam dos provedores pelo gateway e a situação dos webhooks de saída.
Dois fluxos parecidos, em sentidos opostos:
- Pra dentro: provedores (WAHA, Meta) avisam o gateway por webhook. O gateway confere a assinatura e enfileira, e os workers aplicam.
- Pra fora: cada mudança de estado do domínio vira uma linha em
eventse uma entrada no stream da conta, que o SSE entrega ao app.
sequenceDiagram
autonumber
participant P as WAHA / Meta
participant G as gateway :3002
participant PS as provider:nome<br/>(Redis Stream)
participant W as workers
participant DB as tabela events
participant AS as events:account:id<br/>(Redis Stream)
participant API as api
participant App as app
P->>G: webhook assinado
G->>G: confere HMAC ou assinatura
G->>PS: XADD
G-->>P: 200 em milissegundos
W->>PS: XREADGROUP (grupo workers)
W->>W: aplica (pareamento, ingest)
W->>DB: INSERT (a fonte da verdade)
W->>AS: XADD (depois da linha)
W->>PS: XACK
App->>API: GET /accounts/{uid}/events?ticket=…
API->>AS: XREAD a partir do Last-Event-ID
API-->>App: SSE event: event
Note over W,AS: Um handler que lança erro não confirma (XACK)<br/>e a entrada volta na próxima passada.A tabela events
Append-only, criada na migração 0016_events (AD-010). É a fonte da verdade do que aconteceu, e o Redis é só o transporte.
| Coluna | O que é |
|---|---|
id | ULID: ordena por tempo, é único entre processos |
type | Nome do evento, por exemplo pairing.paired |
account_id, seat_id | Conta (sempre) e vaga (quando o evento é de uma pessoa) |
source | Quem gravou: api, gateway, workers ou crons |
payload | JSON. Nunca leva conteúdo de conversa |
occurred_at | Quando foi gravado |
Índice events_by_account (account_id, id). O cliente recebe um EventDto, com o seatUid público no lugar do id interno. A tabela abaixo sai direto do tipo em backend/src/domains/events/dtos/event.dto.ts:
Prop
Type
De mudança de estado a evento
backend/src/domains/events/use-cases/publish-event.ts tem três funções:
| Função | O que faz | Quando usar |
|---|---|---|
publishEvent(input) | Grava a linha e depois faz XADD em events:account:{id} | O caso comum |
recordEvent(input, tx) | Só grava a linha, dentro da transação de quem chama | Quando o evento tem que existir exatamente se a mudança foi confirmada (por exemplo, alertas) |
streamEvent(accountId, dto) | Só faz o XADD | Depois do commit, pra empurrar o que recordEvent gravou |
A linha vem antes do stream de propósito: um stream que corre na frente da tabela poderia anunciar algo que nunca aconteceu. Os domínios embrulham isso em helpers próprios (publishConsentEvent, publishQueueEvent, publishPairingEvent). O de pareamento também escreve no stream da pessoa protegida.
import { publishEvent } from "@domains/events/index.ts";
await publishEvent({
type: "seat.paused",
accountId: seat.account_id,
seat, // vira seatUid no EventDto
payload: { cause: "guardian", pausedAt, until },
source: "api",
});O catálogo dos tipos fica em Tempo real.
Webhooks de entrada (gateway)
O processo gateway (backend/apps/gateway/main.ts) recebe os webhooks, confere, enfileira e responde em milissegundos. Ele nunca toca nas tabelas do domínio: banco lento não pode fazer o provedor desistir da gente.
| Rota | Provedor | Verificação | Vai pra |
|---|---|---|---|
POST /webhooks/waha | WAHA | HMAC-SHA512 do corpo no header X-Webhook-Hmac, com WAHA_WEBHOOK_HMAC_KEY. Sem a chave o gateway nem sobe | provider:waha |
GET /webhooks/whatsapp | Meta (handshake) | hub.verify_token igual a WABA_VERIFY_TOKEN | Responde hub.challenge |
POST /webhooks/whatsapp | Meta (recibos de entrega e respostas) | X-Hub-Signature-256 com WABA_APP_SECRET | provider:meta, uma entrada por evento do corpo |
GET /health | — | — | Estado do Redis |
Assinatura inválida responde 401, e corpo que não é evento do WAHA responde 400. Essas respostas não usam o envelope da API.
Nos workers, consumeProviderEvents lê cada stream como consumer group workers. Um handler que termina confirma a entrada (XACK), e um que lança erro deixa a entrada pendente pra próxima passada. No WAHA, eventos de sessão mudam o estado do pareamento (applyProviderState), e message/message.any entram no ingest. Evento de sessão desconhecida é descartado. Os detalhes de WAHA, GOWS e pareamento ficam em Instances.
Webhooks de saída
Não existem. O backend não manda webhooks pra clientes nem pra terceiros. Quem precisa reagir a um evento é o próprio app, pelo SSE. As notificações pros responsáveis (caixa de entrada, e-mail, push) são outro mecanismo, descrito em Alertas.
Em aberto
- Não há política de expiração de eventos antigos na tabela
events(pendência registrada na AD-010). - O webhook de cobrança
POST /webhooks/asaas(G-33 doapi-gaps.md) ainda não existe. - Webhook de saída pra parceiros nunca foi discutido em AD ou spec. Se aparecer, precisa de decisão própria.
Fontes
backend/src/domains/events/**(publish-event.ts,tables/events.table.ts,dtos/event.dto.ts,entities/event.entity.ts)backend/migrations/0016_events.tsbackend/apps/gateway/main.ts,backend/src/infra/provider-stream.ts,backend/src/infra/streams.tsbackend/apps/workers/consume-provider-events.ts,backend/apps/workers/waha-events.tsbackend/src/domains/channels/webhooks/waha-webhook.ts,meta-webhook.tsbackend/src/domains/channels/use-cases/publish-pairing-event.ts.specs/features/redesign/blueprint.md,api-gaps.md(G-33)- AD-010
