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-pullerrepo: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)
| Item | Value |
|---|---|
| Host | bms-4 (54.36.123.110) |
| Runtime | Docker 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. |
| Logs | docker 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 bucketwa-uploadswith the collector’s own write-only IAM key. A failed download/upload logsmedia 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_devicesand heartbeats top24_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):
| Column | Meaning |
|---|---|
device_id | Capture source (baileys-web[-<suffix>]) |
msg_key_id | Baileys key.id; UNIQUE with device_id |
from_me | key.fromMe |
remote_jid | key.remoteJid — group @g.us / 1:1 @s.whatsapp.net |
ts | messageTimestamp (Unix seconds) → ISO 8601 |
body | Text from conversation / extendedText / image/video/document caption |
media_mime_type | Mimetype of the media sub-object |
media_url | Wasabi key (bucket-relative), null if no media or upload failed |
status | Baileys status |
p24_whatsapp_messages — viewer table (#221/#222), feeds the whatsapp_messages_enriched
view read by et-operational-platform /api/whatsapp/messages:
| Column | Meaning |
|---|---|
id | Baileys key.id (no DB default — reused as PK) |
chat_id | Group JID (NOT NULL) |
sender / sender_name | key.participant / pushName (falls back to sender JID) |
body | Same extraction as above |
has_media / media_type | Boolean / enum image·video·audio·document (stickers → image) |
media_wasabi_key | Same Wasabi key as media_url above |
timestamp | Unix 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_messagesis the table name the decommissioned WAHA incident-router also used (monitoring/supabase/migrations/005_rename_to_p24_whatsapp.sql). Older rows in it (ids likefalse_<jid>_<msgid>_...) are WAHA-era; Baileys rows use the barekey.id.
Wasabi key layout: {WA_COLLECTOR_USER_ID}/{channel}/media/{images|videos|audio|voice-notes|documents}/{tsMs}-{msgKeyId}.{ext}.
1.4 Group scope
- Primary:
whatsapp_groupsrows withmonitored = true(managed in et-operational-platform/admin/whatsapp/groups). Fetched once at startup — restart the container after changes. 2026-10-05: resolved 15 groups. - Fallback:
WA_COLLECTOR_GROUP_JIDS(only if the table fetch fails or returns zero rows). - 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:
- Capture + upload (collector):
p24_whatsapp_messages.media_wasabi_keyis populated for images, video and voice notes. This always worked. - A read-only key on
wa-uploads. et-op’s defaultWASABI_ACCESS_KEY_IDis scoped to et-op’s own bucket and has no grant onwa-uploads. The dedicatedWA_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 bysecrets-sync.yml. - Signing code (et-op):
getWaMediaSignedUrlinsrc/lib/wasabiClient.tssigns with that key (#2283 / #2284) and uses regioneu-central-2with its ownWA_MEDIA_WASABI_REGION/WA_MEDIA_WASABI_ENDPOINT, never the defaulteu-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:
- cAdvisor (job
cadvisor, bms-4,name="wa-web-collector") — already scraping. Container-level alerts work regardless of the collector code. - Collector
/metrics(jobwa_web_collector,54.36.123.110:9477,METRICS_PORT) — the collector’s own gauges/counters. Shipped by the companionwhatsup-android-chat-pullerPR; 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):
| Alert | Severity | Signal |
|---|---|---|
WaCollectorContainerMissing | critical | cAdvisor container_last_seen absent/stale >2m |
WaCollectorRestartLoop | warning | ≥3 container starts in 1h |
WaCollectorTargetDown | warning | up==0 for 5m (process hung / port lost) |
WaCollectorDisconnected | critical | wa_connected==0 for 10m |
WaCollectorLoggedOut | critical | wa_logged_out==1 — human QR re-pair; ban hits all linked devices |
WaCollectorProbeStale | warning | self-probe >45m stale while connected (zombie socket) |
WaCollectorNoMessagesBusinessHours | warning | no msg >6h, Mon-Fri business hours (soft signal) |
WaCollectorPersistErrors | warning | Supabase upsert errors in 15m |
WaCollectorMediaFailureRatio | warning | media failure ratio >20% over 30m (≥5 attempts) |
WaCollectorRetryBacklog | warning | retry 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
| Path | Mechanism | Limitation |
|---|---|---|
JsonIngestRunner | Android, root, reads msgstore.db | Root blocked on the companion Samsung (Knox/VaultKeeper) |
WaNotificationListenerService | Android, 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 withgh workflow enable "<name>"):Run WA DB Processor(cron every 30 min on thebms4-androidrunner, which also held the bms-4 runner-session gate),Run WA Media Correlator(cron every 30 min), andBuild Debug APK. - Still running:
Sync WA ThreadsandGenerate WA Threads(both also process Baileys rows), web deploy, and CI.infra-src/wa-app-apistays 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-onlywa-app-apihandlers (ingest,registerDevice,heartbeat) unless something else uses them by then. Then decommission thebms4-androidrunner.
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-opsync.tswas 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.