whatsup Chat Puller — Consolidated Spec (Baileys-only)

Repo (code): radieu/whatsup-android-chat-puller (private) — this doc lives in p24-infra/docs per this repo’s doc-location convention, even though the implementation is in the other repo. dev_r_services: service_name = 'whatsup-android-chat-puller' (parent) and service_name = 'wa-web-collector' (child row) Decision date: 2026-10-04 — Baileys (wa-web-collector) is the sole real-time capture path we invest in. The two Android-device paths (§2) are kept only as historical/fallback context. Live-state verified: 2026-10-05 (container inspection + Supabase row check on bms-4).

Consolidates three docs from the whatsup-android-chat-puller repo: infra-src/wa-web-collector/README.md, docs/whatsapp-web-collector-risk.md, docs/wa-web-collector-deployment-runbook.md. Those stay canonical for exact code/env-var detail; this doc is the single reference for “what runs, where, and why.”


1. Active path: wa-web-collector (Baileys)

Code: infra-src/wa-web-collector/ in whatsup-android-chat-puller (Node.js/TypeScript, @whiskeysockets/baileys 7.0.0-rc14). Issues #175 (collector) and 222 (second write target p24_whatsapp_messages for the et-operational-platform viewer).

1.1 Deployment (live)

ItemValue
Hostbms-4 (54.36.123.110)
RuntimeDocker container wa-web-collector, restart: unless-stopped
Compose file/opt/wa-web-collector/docker-compose.yml (standalone project — not part of bms-4/docker-compose.yml)
Build context/opt/whatsup-android-chat-puller/infra-src/wa-web-collector (separate repo checkout on bms-4)
Env file/opt/wa-web-collector/.env — rendered from secrets/wa-web-collector.env.sops (no secrets-sync.yml job; manual/worker delivery)
Session (QR)Volume /opt/wa-web-collector/auth → /app/auth. Already paired; deleting it forces a new human QR scan. Treat as secret-equivalent.
Logsdocker logs wa-web-collector (pino JSON, json-file driver, 3×10 MB)

The project lives at /opt/wa-web-collector, outside the /opt/p24-infra git checkout on purpose (#7441): an untracked directory there kept the checkout dirty and made the checkout-freshness gate fail closed. Run docker compose commands from /opt/wa-web-collector. Metrics/health are served on port 9477 (/metrics, /health), reachable only from vps-i1 via a source-restricted ufw rule.

No systemd/pm2 unit — systemctl/pm2 will show nothing; look at Docker.

# status / logs
ssh root@54.36.123.110 "docker ps --filter name=wa-web-collector; docker logs --since 1h wa-web-collector | tail -50"
# apply a changed group allowlist (fetched once at startup) or a new .env
ssh root@54.36.123.110 "cd /opt/wa-web-collector && docker compose restart"
# deploy new code (after pulling the whatsup-android-chat-puller checkout)
ssh root@54.36.123.110 "cd /opt/wa-web-collector && docker compose up -d --build"

Healthy startup log sequence: group allowlist resolved (with count) → opened connection to WA → WhatsApp connection open. Each captured message then logs batch upserted and p24_whatsapp_messages batch upserted.

1.2 What it does

  • Connects as a linked/companion device (one-time QR pairing); session persists in the auth volume.
  • Listens live (event-driven, no polling) for new messages — text and media (images, video, documents, audio, voice notes, stickers) — in monitored groups only.
  • Media: downloadMediaMessage → PUT to Wasabi bucket wa-uploads with the collector’s own write-only IAM key. A failed download/upload logs media download/upload failed — row kept, media_url stays null (level 50) and the row is still written. There is no success log for media — absence of errors is the only log signal; verify via the DB (§1.5).
  • Self-registers in p24_wap_user_devices and heartbeats to p24_wap_device_heartbeat.

1.3 Data model — two tables written per message

wa_device_messages — device-pipeline table (same schema/conflict key device_id,msg_key_id as the legacy Android path):

ColumnMeaning
device_idCapture source (baileys-web[-<suffix>])
msg_key_idBaileys key.id; UNIQUE with device_id
from_mekey.fromMe
remote_jidkey.remoteJid — group @g.us / 1:1 @s.whatsapp.net
tsmessageTimestamp (Unix seconds) → ISO 8601
bodyText from conversation / extendedText / image/video/document caption
media_mime_typeMimetype of the media sub-object
media_urlWasabi key (bucket-relative), null if no media or upload failed
statusBaileys status

p24_whatsapp_messages — viewer table (#221/#222), feeds the whatsapp_messages_enriched view read by et-operational-platform /api/whatsapp/messages:

ColumnMeaning
idBaileys key.id (no DB default — reused as PK)
chat_idGroup JID (NOT NULL)
sender / sender_namekey.participant / pushName (falls back to sender JID)
bodySame extraction as above
has_media / media_typeBoolean / enum image·video·audio·document (stickers → image)
media_wasabi_keySame Wasabi key as media_url above
timestampUnix seconds integer (not ISO — the API range-queries it numerically)

Rows with null chat_id or timestamp are filtered out (isP24RowPersistable) so one malformed message can’t fail a whole 500-row batch.

Name collision note: p24_whatsapp_messages is the table name the decommissioned WAHA incident-router also used (monitoring/supabase/migrations/005_rename_to_p24_whatsapp.sql). Older rows in it (ids like false_<jid>_<msgid>_...) are WAHA-era; Baileys rows use the bare key.id.

Wasabi key layout: {WA_COLLECTOR_USER_ID}/{channel}/media/{images|videos|audio|voice-notes|documents}/{tsMs}-{msgKeyId}.{ext}.

1.4 Group scope

  1. Primary: whatsapp_groups rows with monitored = true (managed in et-operational-platform /admin/whatsapp/groups). Fetched once at startup — restart the container after changes. 2026-10-05: resolved 15 groups.
  2. Fallback: WA_COLLECTOR_GROUP_JIDS (only if the table fetch fails or returns zero rows).
  3. Both empty → captures every chat (logged warning; not a supported production state).

monitored = false = not captured at all (capture-level switch — different from the old WAHA meaning, where it only gated AI processing). whatsapp_groups is one physical table shared with the old WAHA tooling and has multiple writers; see whatsup-admin-ui-et-operational-platform.md.

1.5 Media in the et-op viewer (resolved 2026-10-05)

Media from the collector shows in the et-op WhatsApp viewer. It was invisible for part of 2026-10-05; three things had to be in place:

  1. Capture + upload (collector): p24_whatsapp_messages.media_wasabi_key is populated for images, video and voice notes. This always worked.
  2. A read-only key on wa-uploads. et-op’s default WASABI_ACCESS_KEY_ID is scoped to et-op’s own bucket and has no grant on wa-uploads. The dedicated WA_MEDIA_WASABI_ACCESS_KEY_ID / WA_MEDIA_WASABI_SECRET_ACCESS_KEY (GetObject only, wa-uploads/48616e93-.../*/media/*) were minted in p24-infra#7396 (merged 10:31 UTC) and pushed to both Vercel projects by secrets-sync.yml.
  3. Signing code (et-op): getWaMediaSignedUrl in src/lib/wasabiClient.ts signs with that key (#2283 / #2284) and uses region eu-central-2 with its own WA_MEDIA_WASABI_REGION / WA_MEDIA_WASABI_ENDPOINT, never the default eu-central-1 (#2287 / #2291, merged 10:58 UTC).

If media disappears again, check in this order: the WA_MEDIA_WASABI_* env vars exist in the Vercel project being viewed (the code returns null and logs a warning when they are missing), then the region/endpoint, then that the object key exists in wa-uploads. The collector’s own key is write-only by design — never reuse or widen it for reads.

Quick backend check (run inside the container so it uses its own env; prints keys, no secrets):

ssh root@54.36.123.110 'docker exec wa-web-collector node --input-type=module -e "
import { mintServiceRoleJwt } from \"/app/dist/jwt.js\";
const j = await mintServiceRoleJwt(process.env.SUPABASE_JWT_SECRET);
const r = await fetch(process.env.SUPABASE_URL + \"/rest/v1/p24_whatsapp_messages?select=id,media_type,media_wasabi_key&has_media=eq.true&order=timestamp.desc&limit=10\",
  { headers: { apikey: process.env.SUPABASE_ANON_KEY, authorization: \"Bearer \" + j } });
console.log(await r.text());"'

1.6 Secrets

secrets/wa-web-collector.env.sops — real values present (Wasabi write-only key pair minted; no placeholders as of 2026-10-05). Recipients: developer, lap2 (WSL2), bms-4 worker, lap2-windows. Keys: WASABI_ACCESS_KEY_ID, WASABI_SECRET_ACCESS_KEY, WASABI_S3_ENDPOINT, WASABI_REGION, WASABI_BUCKET, SUPABASE_URL, SUPABASE_JWT_SECRET, SUPABASE_ANON_KEY, WA_COLLECTOR_USER_ID, WA_COLLECTOR_DEVICE_ID, WA_COLLECTOR_GROUP_JIDS. After a SOPS change, re-render /opt/wa-web-collector/.env and docker compose restart.

1.7 Risk

Unofficial WhatsApp Web protocol client — against WhatsApp ToS. Read-only (no sends), but runs from a datacenter IP, and a ban hits every device linked to the account at once (including the legacy Android companions). Accepted by the owner; full detail in docs/whatsapp-web-collector-risk.md (other repo). Monitor the heartbeat; a loggedOut disconnect stops the collector and needs a human QR re-scan (it does not auto-retry).

1.8 Monitoring (#7480)

Prometheus (vps-i1) monitors the collector; alerts route to the standard Alertmanager → Discord/email path. Recovery runbook: docs/playbooks/wa-web-collector-recovery.md — linked from every alert annotation.

Two metric sources:

  1. cAdvisor (job cadvisor, bms-4, name="wa-web-collector") — already scraping. Container-level alerts work regardless of the collector code.
  2. Collector /metrics (job wa_web_collector, 54.36.123.110:9477, METRICS_PORT) — the collector’s own gauges/counters. Shipped by the companion whatsup-android-chat-puller PR; the scrape job goes live only at the bms-4 rollout (see Rollout order below).

Metric contract (shared verbatim with whatsup-android-chat-puller — do not rename): wa_connected (0/1), wa_logged_out (0/1), wa_last_probe_ok_timestamp_seconds, wa_last_message_timestamp_seconds, wa_upsert_errors_total, wa_media_upload_errors_total, wa_media_uploaded_total, wa_retry_buffer_rows.

Alerts (monitoring/prometheus/rules/wa-web-collector.yml, promtool-tested in rules/tests/wa_web_collector_test.yml):

AlertSeveritySignal
WaCollectorContainerMissingcriticalcAdvisor container_last_seen absent/stale >2m
WaCollectorRestartLoopwarning≥3 container starts in 1h
WaCollectorTargetDownwarningup==0 for 5m (process hung / port lost)
WaCollectorDisconnectedcriticalwa_connected==0 for 10m
WaCollectorLoggedOutcriticalwa_logged_out==1 — human QR re-pair; ban hits all linked devices
WaCollectorProbeStalewarningself-probe >45m stale while connected (zombie socket)
WaCollectorNoMessagesBusinessHourswarningno msg >6h, Mon-Fri business hours (soft signal)
WaCollectorPersistErrorswarningSupabase upsert errors in 15m
WaCollectorMediaFailureRatiowarningmedia failure ratio >20% over 30m (≥5 attempts)
WaCollectorRetryBacklogwarningretry buffer non-empty >30m

Every collector-metric rule tolerates the series not existing yet (absent → no fire), so the rules can ship before the collector code.

Independent Discord alert: the wa-app-api CF Worker alertCheck cron (every 5 min) pages Discord on heartbeat staleness (>20 min, with an alerted_at storm guard). It is separate from these Prometheus alerts and does not cover the loggedOut/zombie-socket cases (heartbeat keeps ticking) — the Prometheus WaCollectorLoggedOut/WaCollectorProbeStale rules fill that gap.

Rollout order (#7480): (1) relocate the deployment out of the git checkout and add the healthcheck / port / ufw rule (infra-task, server-operation — pending); (2) merge the companion collector PR that adds /metrics; (3) add the wa_web_collector scrape job to prometheus.yml, publish 9477:9477, ufw allow 9477/tcp from 217.154.82.162, and deploy the Prometheus config via deploy-monitoring-config.yml — all together, so WaCollectorTargetDown never fires on a not-yet-deployed collector. Container-level alerts (WaCollectorContainerMissing/RestartLoop) work as soon as the rules are deployed.


2. Legacy Android paths: DISABLED 2026-10-05, deletion due 2026-12-05

PathMechanismLimitation
JsonIngestRunnerAndroid, root, reads msgstore.dbRoot blocked on the companion Samsung (Knox/VaultKeeper)
WaNotificationListenerServiceAndroid, no-root, parses notifications (#171)No backfill, best-effort, no media

Decision (radieu, 2026-10-05): the path is disabled now, and its code is deleted on 2026-12-05 unless the decision is reversed in whatsup-android-chat-puller#223.

  • Disabled (GitHub workflows in whatsup-android-chat-puller, re-enable with gh workflow enable "<name>"): Run WA DB Processor (cron every 30 min on the bms4-android runner, which also held the bms-4 runner-session gate), Run WA Media Correlator (cron every 30 min), and Build Debug APK.
  • Still running: Sync WA Threads and Generate WA Threads (both also process Baileys rows), web deploy, and CI. infra-src/wa-app-api stays deployed because the p24-wa-a web app also uses it.
  • Human step: stop the app on both phones (uninstall it, or revoke notification-listener access).
  • On 2026-12-05: delete the Android app module, the disabled workflows and release.yml, scripts/wa-media-correlator/, and the Android-only wa-app-api handlers (ingest, registerDevice, heartbeat) unless something else uses them by then. Then decommission the bms4-android runner.

3. Out of scope (separate systems)

  • ZIP extraction pipeline (docs/whatsapp-extraction-pipeline.md, wa_hist_* tables) — retroactive archive + AI threading; unaffected.
  • WAHA incident router (docs/whatsapp-group-monitoring.md) — decommissioned 2026-06-30. Its dead fallback in et-op sync.ts was removed in et-operational-platform#2253 (PR #2292).

4. Open items

  • Stop the Android app on both phones (human).
  • 2026-12-05: delete the Android path code (whatsup-android-chat-puller#223) unless the decision is reversed.
  • Monitoring + self-heal: p24-infra#7480 (Prometheus, alerts, runbook, moving the deployment out of the git checkout) and whatsup-android-chat-puller#224 (metrics, probe/watchdog, retry buffer, truthful heartbeat).
  • Success log line for media uploads (today only failures log) — covered by #224.