{"id":51479258,"url":"https://github.com/arslanstack/whatsapp-baileys","last_synced_at":"2026-07-07T00:02:15.917Z","repository":{"id":367092886,"uuid":"1279180891","full_name":"arslanstack/whatsapp-baileys","owner":"arslanstack","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-24T13:44:02.000Z","size":35,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-24T15:13:23.760Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arslanstack.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-24T12:56:12.000Z","updated_at":"2026-06-24T13:44:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/arslanstack/whatsapp-baileys","commit_stats":null,"previous_names":["arslanstack/whatsapp-baileys"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/arslanstack/whatsapp-baileys","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arslanstack%2Fwhatsapp-baileys","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arslanstack%2Fwhatsapp-baileys/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arslanstack%2Fwhatsapp-baileys/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arslanstack%2Fwhatsapp-baileys/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arslanstack","download_url":"https://codeload.github.com/arslanstack/whatsapp-baileys/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arslanstack%2Fwhatsapp-baileys/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35209350,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-06T02:00:07.184Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-07T00:01:33.587Z","updated_at":"2026-07-07T00:02:15.877Z","avatar_url":"https://github.com/arslanstack.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# wab — WhatsApp (Baileys) engine\n\nA thin, single-purpose Node service that owns one WhatsApp connection (via [Baileys](https://github.com/WhiskeySockets/Baileys)) and exposes a small HTTP API. It does **no** logging, queueing, or business logic — that's [Notify](https://notify.tarkib.co.uk)'s job. wab just connects and sends.\n\nRuns on `127.0.0.1:3210` in PM2 **fork** mode, behind a CloudPanel reverse proxy at `https://wab.tarkib.co.uk` (SSL). Every endpoint requires the `X-Internal-Secret` header (except `/health`).\n\n\u003e **The single most important fact:** wab routes its WhatsApp connection through a **residential IP** (a phone running Tailscale as an exit node). Without this it does not work — see [Why the residential proxy is essential](#why-the-residential-proxy-is-essential).\n\n---\n\n## Table of contents\n- [Why the residential proxy is essential](#why-the-residential-proxy-is-essential)\n- [Architecture](#architecture)\n- [Requirements](#requirements)\n- [API](#api)\n- [Deploy the service](#deploy-the-service)\n- [Set up the residential proxy (Tailscale)](#set-up-the-residential-proxy-tailscale)\n- [Link a WhatsApp number](#link-a-whatsapp-number)\n- [Changing the number](#changing-the-number)\n- [Warm-up \u0026 safe usage](#warm-up--safe-usage)\n- [Operational notes \u0026 gotchas](#operational-notes--gotchas)\n\n---\n\n## Why the residential proxy is essential\n\nWhatsApp **silently restricts Baileys companion devices that connect from datacenter / VPS IPs.** The symptom is nasty and confusing:\n\n- The socket connects fine and `/api/send` returns `success` with a real message ID…\n- …but the messages **never propagate** — not to the recipient, and **not even to the primary phone's own chat list**.\n- Meanwhile a *genuine* WhatsApp Web/Desktop device on the same account works perfectly, and manual phone messages send fine.\n\nSo the **account is healthy** — WhatsApp is specifically dropping the unofficial companion's traffic because it originates from a flagged datacenter IP. No browser-identity change, presence flag, or re-link fixes this.\n\n**The fix:** make wab's WhatsApp connection exit from a **residential IP**. We do this by running [Tailscale](https://tailscale.com) on a phone (as an *exit node*) and routing **only wab's WhatsApp socket** through it via a local SOCKS proxy. WhatsApp then sees the companion on a normal residential IP, co-located with the primary phone, and everything propagates.\n\nTwo hard-won rules that go with this:\n1. **Use an aged, warmed-up number** — a fresh/temp number gets burned almost immediately. (We burned one permanently during development.)\n2. **Link once, gently, and never churn it** — repeated re-link/logout cycles are what flag a number, *more* than the IP. Leave a working session alone.\n\n## Architecture\n\n```\n                       https://notify.tarkib.co.uk            https://wab.tarkib.co.uk\n                                  │                                      │\n                          ┌───────▼────────┐                    ┌────────▼─────────┐\n                          │ Notify (Laravel)│  localhost call    │ CloudPanel proxy │\n                          │ API · queue ·   │───X-Internal-Secret│   → 127.0.0.1:3210│\n                          │ logging · UI    │                    └────────┬─────────┘\n                          └─────────────────┘                             │\n                                                              ┌───────────▼────────────┐\n                                                              │ wab (Node, PM2 fork)    │\n                                                              │ Baileys socket          │\n                                                              │   └─ agent: SOCKS5 ─────┐│\n                                                              └─────────────────────────┘│\n                                                                                         │ socks5h://127.0.0.1:1055\n                                                              ┌──────────────────────────▼┐\n                                                              │ tailscaled (userspace, PM2)│\n                                                              │ SOCKS5 :1055 → exit node   │\n                                                              └──────────────┬─────────────┘\n                                                                             │ Tailscale\n                                                              ┌──────────────▼─────────────┐\n                                                              │ Phone (Tailscale exit node)│\n                                                              │ home WiFi · residential IP │\n                                                              └──────────────┬─────────────┘\n                                                                             ▼\n                                                                       WhatsApp servers\n```\n\nOnly wab's WhatsApp traffic goes through the phone. Everything else on the server (Notify, system, updates) uses its normal connection — and if the phone goes offline, **only wab** is affected.\n\n## Requirements\n\n- **An aged, warmed WhatsApp number** registered on a primary phone (Baileys links as a *companion*; it does not register numbers). Not a fresh/temp number.\n- **A phone to act as the Tailscale exit node**, kept **at home on stable WiFi, plugged in** (mobile data causes connection flapping — see gotchas).\n- **VPS:** Node 20+, PM2, a CloudPanel reverse-proxy site with SSL.\n- **A Tailscale account** (free tier is enough).\n\n## API\n\n| Method | Path | Body | Returns |\n|---|---|---|---|\n| `GET` | `/health` | — | `{ status, state }` (no auth) |\n| `GET` | `/api/status` | — | `{ state, phone, since, hasQr }` |\n| `POST` | `/api/pair` | `{ number }` | `{ success, code }` — 8-char pairing code |\n| `POST` | `/api/send` | `{ to, message }` | `{ success, id, to }` |\n| `POST` | `/api/reconnect` | — | `{ success }` |\n| `POST` | `/api/logout` | — | `{ success }` |\n\n`state`: `connecting` · `open` · `logged_out`. `/api/send` errors: `409` not connected · `400` not on WhatsApp · `422` missing fields. All endpoints except `/health` require header `X-Internal-Secret`.\n\n## Deploy the service\n\nOn the VPS, as the **site user** (e.g. `tarkib-wab`) — no root needed:\n\n```bash\ncd ~/htdocs \u0026\u0026 git clone \u003crepo-url\u003e wab.tarkib.co.uk \u0026\u0026 cd wab.tarkib.co.uk\nnpm ci\n\ncp .env.example .env\nnode -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"   # → INTERNAL_SECRET\nnano .env     # set INTERNAL_SECRET and WAB_PROXY=socks5h://127.0.0.1:1055\n```\n\nPM2 (install once as root: `npm install -g pm2`):\n\n```bash\npm2 start ecosystem.config.cjs    # runs wab in fork mode\npm2 save\npm2 startup                        # run the printed sudo command as root, then `pm2 save` again\n```\n\nIn CloudPanel: create a **Reverse Proxy** site for `wab.tarkib.co.uk` → `http://127.0.0.1:3210`, issue Let's Encrypt SSL.\n\n## Set up the residential proxy (Tailscale)\n\nThis is what makes wab actually work. Two sides: the phone (exit node) and the VPS (SOCKS proxy through it).\n\n### On the phone\n\n1. Install the **Tailscale** app, sign in (same account you'll use on the VPS).\n2. Enable **\"Use as exit node\"** (app menu / Settings → Exit Node → Run as exit node).\n3. Approve it: **https://login.tailscale.com/admin/machines** → the phone → ⋯ → **Edit route settings** → tick **Use as exit node** → Save.\n4. Harden so Android doesn't kill it:\n   - **Always-on VPN**: Android Settings → VPN → Tailscale (gear) → Always-on VPN **ON** (leave \"Block connections without VPN\" **OFF**).\n   - Battery: set Tailscale to **Unrestricted** + allow background + auto-launch.\n   - **Lock** Tailscale in the recent-apps list so \"clear all\" doesn't kill it.\n   - WiFi → **Keep WiFi on during sleep = Always**.\n   - **Keep the phone on home WiFi and plugged in.** (Mobile data / network switching causes flapping.)\n\n### On the VPS (as the site user, no root — userspace mode)\n\n```bash\ncd ~\ncurl -fsSL https://pkgs.tailscale.com/stable/tailscale_latest_amd64.tgz | tar xz   # use arm64 if uname -m = aarch64\nmkdir -p ~/ts-state\n\n# Run tailscaled in userspace mode with a SOCKS proxy, under PM2:\npm2 start $HOME/tailscale_*_amd64/tailscaled --name tailscaled -- \\\n  --tun=userspace-networking \\\n  --socks5-server=127.0.0.1:1055 \\\n  --statedir=$HOME/ts-state \\\n  --socket=$HOME/ts-state/tailscaled.sock\npm2 save\n\n# Connect, using the phone as exit node (get the phone's Tailscale IP from the admin console):\n$HOME/tailscale_*_amd64/tailscale --socket=$HOME/ts-state/tailscaled.sock up \\\n  --exit-node=\u003cPHONE_TAILSCALE_IP\u003e --hostname=wab-vps\n# → open the printed URL, sign in with the same Tailscale account\n```\n\n**Verify** the proxy exits the phone's residential IP:\n\n```bash\ncurl --socks5-hostname 127.0.0.1:1055 https://api.ipify.org   # → the phone's IP\ncurl https://api.ipify.org                                    # → the VPS's IP (should differ)\n```\n\nThen set `WAB_PROXY=socks5h://127.0.0.1:1055` in wab's `.env` and `pm2 restart wab`. wab logs `Routing WhatsApp through proxy …` on start.\n\n\u003e Note: in userspace mode, system tools like `ping` cannot reach Tailscale `100.x` addresses — only the SOCKS proxy can. That's expected and is the point (nothing system-wide is touched).\n\n## Link a WhatsApp number\n\n**QR scanning does not work from a server** — use the **pairing code**. The number must already be registered on a primary phone.\n\nFrom the Notify dashboard (`/wab`): **Generate pairing code** with the number → on the phone, **WhatsApp → Linked Devices → Link a Device → \"Link with phone number instead\"** → enter the code. (A \"might be a scam\" warning is normal for this flow.) Or via API:\n\n```bash\ncurl -s -X POST -H \"X-Internal-Secret: \u003csecret\u003e\" -H \"Content-Type: application/json\" \\\n  -d '{\"number\":\"\u003cinternational digits\u003e\"}' http://127.0.0.1:3210/api/pair\n```\n\nConfirm with `/api/status` → `\"state\":\"open\"`. The session persists in `auth/`; restarts reconnect with no re-link. **Link once and leave it alone.**\n\n## Changing the number\n\n1. Register/age the new number on its own primary phone first.\n2. `POST /api/logout` (or dashboard **Re-link / Logout**) — wipes the session.\n3. `POST /api/pair` with the new number, enter the code on its phone.\n4. Verify `/api/status` shows `open` with the new number. No redeploy needed.\n\n## Warm-up \u0026 safe usage\n\nThe number works, but stay un-flagged by behaving like a human:\n\n- **Only message people who expect it.** Recipients blocking/reporting you is the #1 ban trigger — far more than volume.\n- **Ramp gradually:** ~10–20 msgs/day week 1, ~30–50/day week 2, increase slowly after. Never sudden spikes.\n- **Two-way conversations** (replies) look natural; pure one-way blasting is a flag.\n- **Vary content** — identical bulk text looks like spam. Personalize.\n- **Don't re-link** a working session; churn is what burns numbers.\n- Internal/transactional notifications to known contacts (staff, opted-in customers) is the safest, lowest-risk use case.\n\nIf the session starts logging out repeatedly or messages stop propagating, WhatsApp is pushing back — **back off the volume and let it rest.**\n\n## Operational notes \u0026 gotchas\n\n- **Fork mode only.** PM2 `exec_mode: 'fork'`. Cluster mode breaks the single stateful socket (code-408 reconnect loop). Never `instances \u003e 1`.\n- **`trust proxy` is set** (`app.set('trust proxy', 1)`) because it's behind a reverse proxy — otherwise `express-rate-limit` throws on `X-Forwarded-For`.\n- **Phone must be on stable WiFi.** Mobile data / network-switching causes constant code-408 `connectionLost` flapping (keepalives time out over the jittery path). WiFi + plugged in is stable.\n- **Both `wab` and `tailscaled` run under PM2** as the site user; `pm2 save` + `pm2 startup` make them survive reboots.\n- **The 14-day rule:** the primary phone must connect to WhatsApp at least every couple of weeks or all linked devices get logged out.\n- **Brief drops are absorbed:** real traffic goes through Notify's queue (`SendWabJob`, 3 retries + backoff), so momentary reconnects don't lose messages. Direct `curl` to wab has no retry and will show `409` during a blip.\n- **Code 515** right after first link is normal (restart-required handshake) → auto-reconnects to `open`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farslanstack%2Fwhatsapp-baileys","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farslanstack%2Fwhatsapp-baileys","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farslanstack%2Fwhatsapp-baileys/lists"}