WAHA e GOWS
Como o Superdinda fala com o WhatsApp — WAHA self-hosted com a engine GOWS, uma sessão por pareamento e um fluxo orientado a eventos.
O lado WhatsApp do Superdinda não usa a API oficial para as contas pessoais das pessoas protegidas: não existe alternativa oficial para isso. Cada WhatsApp conectado é um dispositivo vinculado dentro de um WAHA que nós hospedamos. A API oficial (Meta Cloud API) só aparece para enviar o link de consentimento, e passa por outro webhook.
O que é o WAHA
WAHA (devlikeapro/waha, Apache-2.0) é uma API HTTP
sobre o protocolo do WhatsApp Web, distribuída como imagem Docker. Desde a versão
2026.6.1 a imagem pública traz multi-sessão, todas as engines, proxy por sessão e
sessões em Postgres sem custo. Nós falamos com ele por HTTP (header X-Api-Key) e
ele fala conosco por webhooks assinados.
Por que a engine GOWS
A engine é fixada em WHATSAPP_DEFAULT_ENGINE: GOWS nos dois compose. GOWS é a
engine sobre o whatsmeow (Go):
websocket, sem navegador, cerca de 50 MB por sessão.
| Alternativa | Por que ficou de fora (AD-002) |
|---|---|
| Evolution API | v2.4.0+ exige ativação num servidor de licença (503 até ativar); só Baileys |
| whatsapp-web.js / wppconnect | Chromium por sessão, 200–400 MB cada |
| Baileys direto no processo | acopla um protocolo instável ao nosso código; relatos de leak com centenas de sessões; fica como plano B |
| Container por instância | caro de orquestrar, sem ganho anti-ban |
O fallback previsto é a engine NOWEB (Baileys) dentro do próprio WAHA, sem mudar nosso código, porque os webhooks abstraem a engine.
Em aberto
O fallback para NOWEB nunca foi exercitado. O payload dos eventos varia por engine, e o adapter foi conferido só contra GOWS (WAHA 2026.8.2).
Sessões
- Uma sessão WAHA por pareamento, não por pessoa.
startPairingcria a sessão com o nomesd-<pairingUid>(oprovider_refda linha empairings). Um novo pareamento do mesmo canal abre uma sessão nova. - O pareamento pertence a um
seat_channel(vaga × canal). Só pode haver um pareamento vivo (ended_atnulo) porseat_channel. - Cada sessão nasce com o próprio proxy (
config.proxy), o próprio webhook (config.webhooks, eventossession.statusemessage.any, chave HMAC) econfig.metadata.pairingUid. Ver Proxies. - As sessões ficam no Postgres próprio do WAHA (serviço
waha-db,WHATSAPP_SESSIONS_POSTGRESQL_URL), separado do nosso banco: o histórico chega lá em claro antes de o ingest cifrar a nossa cópia. WHATSAPP_RESTART_ALL_SESSIONS: "true"faz o WAHA restaurar todas as sessões do banco ao subir.
Fluxo orientado a eventos (AD-010)
WhatsApp ⇄ WAHA (GOWS · uma sessão por pareamento · proxy por sessão)
│ POST /webhooks/waha header X-Webhook-Hmac (HMAC-SHA512 do corpo cru)
▼
gateway (apps/gateway, :3002)
│ verifica HMAC → XADD provider:waha { eventId, body } → 200 em ms
▼
Redis Stream provider:waha (consumer group "workers", MAXLEN ~50000)
▼
workers (apps/workers)
├─ session.status → signalFromWaha → applyProviderState (máquina de estados)
├─ message / message.any → ingestLiveMessage (ingest ao vivo)
├─ challengeRefresher (a cada 10 s: QR, código expirado, passkey)
└─ reconnectTicker (a cada 15 s: reinicia sessões degraded com retry vencido)
▼
domínios: channels · proxies · ingest · notifications · events (SSE do app)| Etapa | Onde | O que garante |
|---|---|---|
| Assinatura | verifyWahaSignature | comparação em tempo constante; sem header → 401 |
| Gateway sem domínio | apps/gateway/main.ts | banco lento nunca faz o WAHA desistir do webhook |
| Boot fail-closed | apps/gateway/main.ts | sem WAHA_WEBHOOK_HMAC_KEY o gateway não sobe |
| Dedupe | PairingEventEntity.record | um provider_event_id só move o pareamento uma vez |
| Sessão desconhecida | handleWahaEvent | evento de sessão sem pareamento vivo é descartado |
| Retry do WAHA | config.webhooks[].retries | exponencial, 2 s, 10 tentativas |
O mesmo gateway recebe os webhooks da Meta (GET/POST /webhooks/whatsapp, stream
provider:meta) para recibos dos links de consentimento. Não é o canal das sessões.
Em aberto
Um evento cujo handler lança fica pendente no consumer group, mas o consumidor só
lê entradas novas (XREADGROUP … >) e não há XPENDING/XAUTOCLAIM no código.
Na prática, uma entrada que falhou não é relida sozinha.
Nunca enviar
Regra de produto (AD-002): a instância só lê. Nenhum caminho de código chama um endpoint de envio do WAHA. O WAHA só busca o QR/código, o estado, a foto de perfil do próprio número conectado e o histórico. Ver Anti-ban.
Rodando local
cp .env.example .env # WAHA_IMAGE_TAG=arm no Apple Silicon
docker compose up -d # postgres, redis, waha (3001), waha-db, minio
bun run gateway # :3002, o WAHA posta em host.docker.internal:3002
bun run workers # consome provider:waha
bun run pair:qr -- <seat-uid> # grava o QR atual num PNG (só dev, lê o banco direto)O dashboard do WAHA fica em http://localhost:3001/dashboard, atrás do basic auth
do Swagger (WAHA_DASHBOARD_USERNAME / WAHA_DASHBOARD_PASSWORD). Senhas fracas
(admin, waha, 123…) são trocadas por uma aleatória no boot, e o navegador
fica em loop pedindo senha. Sem WAHA_URL/WAHA_API_KEY o canal whatsapp
responde 503.
Em aberto
Sharding com mais de um container WAHA (WAHA_WORKER_ID distinto,
assignedWorker) está previsto em AD-002, mas ainda não foi validado na
prática.
Fontes
backend/apps/gateway/main.ts,backend/apps/workers/{main,waha-events,consume-provider-events,challenge-refresher,reconnect-ticker}.tsbackend/src/infra/{streams,provider-stream}.tsbackend/src/domains/channels/adapters/waha-adapter.ts,webhooks/waha-webhook.ts,use-cases/start-pairing.tsbackend/docker-compose.yml,backend/docker-compose.prod.yml,backend/README.md,backend/src/config/env.ts- Vault: AD-002 (waha-gows-proxy-por-sessao), AD-010 (pairing event-driven),
research/2026-09-11-whatsapp-nao-oficial,problems/2026-09-11-waha-dashboard-loop-basic-auth,_state(multi-worker pendente)
