Arquitetura · Ingest resistente a falhas · ADR-0003

Coletor WhatsApp Datalake He4rt

Um pipeline resistente a falhas por construção: o bot envia o evento cru, com idempotência determinística e fila durável; o backend grava síncrono (o 2xx só sai depois do commit).

garantia 01
Sem perda

O 2xx só vem após o commit. O que não confirma fica no outbox durável e é re-tentado.

garantia 02
Sem duplicata

O event_id determinístico deduplica o mesmo evento na origem e no destino.

garantia 03
Só o que importa

O filtro de borda corta sessão, firehoses e DMs — e grava o resto cru.

01

O contexto

A He4rt quer enxergar o engajamento dos membros nos grupos do WhatsApp, do mesmo jeito que já faz no Discord. São dois processos separados, como no Discord o runtime e o ingest são apartados:

objetivo
datalake de dados importantes, resistente a falhas. Modelagem (grupos, membros, métricas) é derivada depois (schema-on-read).
wpp-tui · runtime

Segura a sessão Baileys (WhatsApp Web), filtra na borda e encaminha o evento cru. É a ponte — quase "burro".

integration-whatsapp · ingest

Monolito Laravel. Valida, sanitiza e grava cru numa tabela única (whatsapp_event_logs).

02

O fluxo, ponta a ponta

Um evento atravessa três zonas — bot, rede, backend — até virar uma linha no lake. O pulso desce o trilho no sentido do fluxo.

03

Idempotência em dose dupla

O event_id é um UUIDv5 determinístico por conteúdo, calculado no bot. Mesmo evento lógico → mesmo id. Isso arma a dedup duas vezes:

① na origem · bot

event_id é PK do outbox SQLite. INSERT OR IGNORE descarta re-emit idêntico antes de ir à rede.

② no destino · backend

event_id é UNIQUE. firstOrCreate ignora duplicata se a resposta se perdeu e o bot reenviou.

⚠ por que não um id aleatório

O ID aleatório (decisão revertida) fazia todo re-emit do Baileys — reconexão, append offline, re-snapshot de groups.metadata a cada open — virar linha nova. Re-emit não é raro: é recorrente.

demo ao vivo

Uma reação no grupo, calculada de verdade

UUIDv5 · mesmo algoritmo do bot

Chave da reação = (tipo, id da msg, quem reagiu, emoji). O senderTimestampMs fica de fora (instável entre re-emissões). Clique nos emojis e veja o lake reagir:

event_id calculado
lake · linhas distintas

Chave determinística por tipo

tipochave canônica

"canônico" = JSON com chaves ordenadas e participants ordenados por id — senão cada reconexão reordena e o snapshot vira "novo", derrotando a dedup.

04

As decisões — e os trade-offs

Cada card mostra a opção escolhida, a descartada, o porquê e o custo aceito.

05

O que salvamos — e o que não

A regra é default-allow com denylist: passa o que é de grupo (@g.us) e não está na lista de corte. "Importante" = tudo que não é o ruído conhecido.

Salvos

passam o filtro

Não salvos

cortados — com o motivo
a lógica do corte

Firehoses são cortados por volume altíssimo × valor de coleta baixo (veja as barras). A sessão, por segurança. DMs, por escopo. O resto passa cru.

06

E quando algo falha?

Seis cenários, o caminho de cada um e o veredito. Em todos: nada importante se perde.

07

O que ganhamos

  • Sem perda e sem duplicatas — dedup dupla + 2xx-pós-commit + tolerância a poison + dead-letter.
  • Backend mais simples — o job assíncrono morreu; ingest é uma Action síncrona fina.
  • Sem mudança de schema — event_id uuid UNIQUE já comporta UUIDv5.
  • Resistência independe do driver de fila (Redis é app-wide, não do lake).
diferido (de propósito)
  • Anti-replay, throttle, limite de corpo, rotação de segredo.
  • Backfill profundo de histórico (limitado no dispositivo acompanhante).
  • Deploy/hospedagem, vinculação de identidade, métricas.
  • Governança/LGPD e retenção.