SuperdindaDocs

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.

AlternativaPor que ficou de fora (AD-002)
Evolution APIv2.4.0+ exige ativação num servidor de licença (503 até ativar); só Baileys
whatsapp-web.js / wppconnectChromium por sessão, 200–400 MB cada
Baileys direto no processoacopla um protocolo instável ao nosso código; relatos de leak com centenas de sessões; fica como plano B
Container por instânciacaro 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. startPairing cria a sessão com o nome sd-<pairingUid> (o provider_ref da linha em pairings). 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_at nulo) por seat_channel.
  • Cada sessão nasce com o próprio proxy (config.proxy), o próprio webhook (config.webhooks, eventos session.status e message.any, chave HMAC) e config.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)

Do WhatsApp até os domínios
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)
EtapaOndeO que garante
AssinaturaverifyWahaSignaturecomparação em tempo constante; sem header → 401
Gateway sem domínioapps/gateway/main.tsbanco lento nunca faz o WAHA desistir do webhook
Boot fail-closedapps/gateway/main.tssem WAHA_WEBHOOK_HMAC_KEY o gateway não sobe
DedupePairingEventEntity.recordum provider_event_id só move o pareamento uma vez
Sessão desconhecidahandleWahaEventevento de sessão sem pareamento vivo é descartado
Retry do WAHAconfig.webhooks[].retriesexponencial, 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

backend/
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}.ts
  • backend/src/infra/{streams,provider-stream}.ts
  • backend/src/domains/channels/adapters/waha-adapter.ts, webhooks/waha-webhook.ts, use-cases/start-pairing.ts
  • backend/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)

On this page