Self-hosting
The hosted service is a convenience, not a moat. Everything it does, you can run on a spare machine in about twenty minutes. This page is the honest version of that walkthrough, including the parts that are annoying.
What you need
| Requirement | Why |
|---|---|
| A machine that stays awake | The sync daemon must hold a connection; a laptop that sleeps loses history |
| Go 1.24+ | Builds the wacli binary |
| Node 20+ | Runs the gateway |
| ~1 GB disk per busy account | SQLite store plus any media you download |
| A tunnel or reverse proxy | Only if you want to reach it from outside the machine |
1 · Build the CLI
git clone https://github.com/openclaw/wacli cd wacli go build -tags sqlite_fts5 -ldflags="-s -w" -o ~/bin/wacli ./cmd/wacli ~/bin/wacli --version
The build tag matters. Without -tags sqlite_fts5 the store migration fails with no such module: fts5, and full-text search is gone. It is the single most common setup mistake.
2 · Link an account
wacli --store ~/wa-store auth
A QR code appears in the terminal; scan it in WhatsApp under Settings → Linked devices. The code expires after about a minute, so have the phone ready. Over SSH the block graphics are often unscannable — link from a real terminal, or render the code to an image first.
The store directory now holds device credentials. Anyone who copies it can read that account: chmod 700 it, and keep it off shared volumes and backups you do not control.
3 · Keep it synced
wacli --store ~/wa-store sync
This is a long-running process, not a cron job. It holds the store lock, streams new messages in, and backfills history. Without it running, your store freezes at whatever it last fetched — the second most common setup mistake.
Run it under whatever your system uses to keep services alive: a launchd agent on macOS, a systemd unit on Linux.
# /etc/systemd/system/wacli-sync.service
[Unit]
Description=wacli sync
After=network-online.target
[Service]
ExecStart=/home/you/bin/wacli --store /home/you/wa-store sync
Restart=always
RestartSec=10
User=you
[Install]
WantedBy=multi-user.target
4 · Run the gateway
npm install --prefix server PORT=8787 HOST=127.0.0.1 node server/server.mjs
Tenants live in a JSON file that the process re-reads every few seconds, so adding an account needs no restart:
{
"tenants": [
{ "id": "me",
"token": "<openssl rand -hex 32>",
"store": "/home/you/wa-store",
"allowSend": false }
]
}
| Variable | Default | Meaning |
|---|---|---|
PORT | 8787 | Listen port |
HOST | 127.0.0.1 | Bind address — keep it on loopback |
WACLI_BIN | ./bin/wacli | Path to the CLI |
WACLI_ME_TENANTS | ./config/tenants.json | Tenant file |
5 · Expose it — carefully
If the client runs on the same machine, stop here: http://127.0.0.1:8787/mcp is all you need, and nothing is reachable from outside.
To reach it from elsewhere, put a TLS terminator in front and leave the service on loopback. A Cloudflare tunnel does this without opening a port or exposing your home IP:
cloudflared tunnel --url http://127.0.0.1:8787
Never bind the service to 0.0.0.0 on a public network. The token is the only thing between the internet and your message history, and a plaintext HTTP hop hands it to anyone on the path.
Operational reality
Sessions drop. WhatsApp logs linked devices out now and then. Watch account_status; re-linking means another QR scan, in person.
Old CLI versions get refused. A client outdated error in the sync log means rebuilding from a current tag. Pin a version you tested.
One writer per store. The sync daemon holds the lock. Read commands are fine alongside it; a second writer is not.
Back up deliberately. The store is one directory. Copying it copies the credentials too — encrypt the backup or skip it.