Skip to content

davesheffer/Relay

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ›°οΈ Relay

A thin, self-hostable bridge between the world's most popular green chat app 🟒 and HTTP. Inbound messages become webhooks. An HTTP call sends one back out. Many numbers, one small Node process, one SQLite file.

(You know the one. We won't say it. Rhymes with "WhatsUpp". Powered by Baileys.)

  🟒 the app                       Relay  (your VPS)                     Your stuff
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚  πŸ“± phone  β”‚ ── messages ─▢ β”‚  Baileys socket(s)   β”‚ ── webhook ─▢ β”‚  n8n / your API β”‚
 β”‚  account  β”‚ ◀── send ───── β”‚  REST + queue + UI   β”‚ ◀── POST /send β”‚  automations    β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚  SQLite (sessions)   β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Relay wraps Baileys (the web protocol of that green messenger) in a tiny Fastify REST layer. It runs as a single long-lived process β€” perfect next to an n8n instance on the same box β€” and ships with a built-in control panel so you never touch the terminal to link a number or send a test message.


✨ What you get

  • Multiple connections, one process β€” create / start / stop / delete chat links at runtime, each its own number.
  • Two ways to link β€” scan a QR, or punch in a phone number and get an 8-char pairing code. No screenshots of terminals.
  • Inbound β†’ webhook β€” every message POSTs a clean JSON payload to a URL you choose (global, or per connection).
  • Outbound β†’ one HTTP call β€” POST /sessions/:id/send, rate-limited by a per-connection queue so you never burst.
  • Survives restarts β€” sessions, auth state, and a message log live in SQLite; connections reconnect on boot with no re-scan.
  • Built-in control UI β€” served by the bridge itself, same origin, no build step.
  • Private by default β€” binds to 127.0.0.1, mutating routes need a secret.

πŸš€ Quick start

Needs Node.js 22+ (Relay uses the built-in node:sqlite β€” no native build).

git clone https://github.com/davesheffer/Relay.git relay && cd relay
npm install
cp .env.example .env        # set API_SECRET + WEBHOOK_SECRET (any long random strings)
npm run build
npm start

Open http://127.0.0.1:3100, paste your API_SECRET (top-right), and Add a connection. That's it.

Deploying to a server? β†’ DEPLOY.md (any Ubuntu VPS; tunnel or domain+HTTPS).


πŸŽ›οΈ The control panel

The bridge serves the UI at / β€” just open its address. Same origin, so no CORS, no separate static server.

  1. Top-right β€” set the base URL (auto-filled to wherever the page is served) and your x-api-secret. Saved locally in your browser.

  2. Add a connection β€” give it a name. Then link it one of two ways:

    Leave phone empty Enter a phone number
    A QR appears β†’ open the app β†’ Linked devices β†’ Link a device β†’ scan You get an 8-char code β†’ open the app β†’ Linked devices β†’ Link with phone number instead β†’ type it
  3. Status flips to connected β†’ the Send box activates.

  4. Watch Recent messages stream in, set a per-connection webhook, or Stop / Start / Delete β€” all live.

The QR is rendered in your browser from the raw string (vendored, no CDN). Read views are open (localhost bind); anything that changes state sends the secret.


🧩 API

Base: http://127.0.0.1:3100 Β· πŸ”’ = needs header X-Api-Secret

Method Path Does
GET /health { ok, sessions }
GET /sessions list connections + live status
GET /sessions/:id one connection
GET /sessions/:id/qr { qr } (string or null)
GET /sessions/:id/messages recent 50 from the log
POST /sessions πŸ”’ create + start Β· { name, phone?, webhookUrl?, webhookSecret? }
POST /sessions/:id/pair πŸ”’ link by number Β· { phone }
POST /sessions/:id/webhook πŸ”’ set its webhook Β· { url, secret? }
POST /sessions/:id/start Β· /stop πŸ”’ start / stop
POST /sessions/:id/send πŸ”’ { jid, text } β†’ { id }
DELETE /sessions/:id πŸ”’ stop + purge auth, messages, row
# create a connection
curl -X POST http://127.0.0.1:3100/sessions \
  -H "content-type: application/json" -H "X-Api-Secret: $API_SECRET" \
  -d '{"name":"Main line"}'
# -> { "id": "main-line-ab12cd", "status": "connecting", ... }

# send a message  (bare numbers get @s.whatsapp.net appended)
curl -X POST http://127.0.0.1:3100/sessions/main-line-ab12cd/send \
  -H "content-type: application/json" -H "X-Api-Secret: $API_SECRET" \
  -d '{"jid":"972501234567","text":"hello from Relay"}'
# -> { "id": "3EB0..." }

# link by phone instead of QR -> status "pairing", code in pairingCode
curl -X POST http://127.0.0.1:3100/sessions/main-line-ab12cd/pair \
  -H "content-type: application/json" -H "X-Api-Secret: $API_SECRET" \
  -d '{"phone":"972501234567"}'

status ∈ connecting · qr · pairing · open · logged_out · closed. Send errors: 400 bad body · 401 bad secret · 404 unknown session / number not registered · 503 not connected. The recipient is verified to exist before sending; the send is queued with SEND_DELAY_MS spacing.


πŸ“₯ Webhooks (inbound)

Each connection can have its own webhook (set at create, in the UI, or via POST /sessions/:id/webhook). No per-connection URL? It falls back to the global WEBHOOK_URL. For every non-fromMe message, Relay fires:

{
  "session":   "main-line-ab12cd",
  "from":      "972501234567",
  "jid":       "972501234567@s.whatsapp.net",
  "text":      "incoming text",
  "timestamp": 1735500000000,
  "messageId": "3EB0XXXXXXXXXXXX",
  "pushName":  "Alice"
}

Sent with header X-Webhook-Secret. Fire-and-forget with a short timeout β€” failures are logged, never crash the socket.


πŸ”— Wiring up n8n

Run n8n on the same box (localhost). See deploy/n8n.service for a systemd unit.

  • Receive β€” Webhook trigger node, method POST, path whatsapp-in. Point each connection's webhook at http://127.0.0.1:5678/webhook/whatsapp-in. Validate X-Webhook-Secret. Branch on {{$json.body.session}} if you run several numbers.
  • Send β€” HTTP Request node β†’ POST http://127.0.0.1:3100/sessions/<id>/send, header X-Api-Secret, body { "jid": "...", "text": "..." }.

πŸ—„οΈ How sessions survive restarts

Baileys keeps a long-lived socket plus an auth state (device creds + a signal key store it mutates constantly). The stock useMultiFileAuthState dumps that to a folder per connection β€” fine for one bot, not a datastore.

Relay implements a custom auth state (src/auth-state.ts) backed by SQLite, scoped per sessionId:

  • creds β†’ one row, rewritten on every creds.update
  • signal keys β†’ one row each (category, doc_id), upserted/deleted as Baileys reads & writes them β€” serialized with Baileys' BufferJSON

On boot, SessionManager.restoreAll() loads every row and auto-starts the flagged ones β€” they reconnect without a new QR. On loggedOut, that connection's auth is purged so the next start shows a fresh code.

table holds
sessions id, name, status, jid, phone, webhook, autostart, timestamps
auth_state creds + signal keys, scoped by session
messages inbound/outbound log (jid, message id, text, ts)

It's all one folder β€” back up data/ + .env and you've backed up everything.


βš™οΈ Configuration

All via .env (see .env.example):

var default what
PORT / HOST 3100 / 127.0.0.1 bind address (keep localhost)
API_SECRET β€” required; gates mutating routes
WEBHOOK_URL / WEBHOOK_SECRET β€” global/fallback webhook
SEND_DELAY_MS 1500 min gap between sends, per connection
WEBHOOK_TIMEOUT_MS 5000 inbound delivery timeout
DB_PATH ./data/relay.db SQLite file
WEB_DIR ./web static UI dir (empty to disable)
CORS_ORIGINS β€” extra browser origins (localhost always allowed)
LOG_LEVEL info pino level (message bodies never logged at info)

πŸ” Security notes

  • Relay binds localhost. The read endpoints (/qr, /sessions, /messages) are intentionally open for local use β€” never expose port 3100 to the internet directly. A public /qr lets a stranger link your account.
  • To reach it remotely: an SSH tunnel, or a reverse proxy with auth over the whole site (see deploy/Caddyfile). Both covered in DEPLOY.md.
  • Secrets live only in your .env (gitignored). The X-Api-Secret check is constant-time.

πŸ› οΈ Stack

TypeScript (ESM) Β· Fastify Β· Baileys Β· pino Β· node:sqlite (Node 22+) Β· zero native dependencies.

⚠️ Disclaimer

Relay is an independent, unofficial project. It is not affiliated with, endorsed by, or connected to Meta Platforms, Inc. or the messaging app it talks to. All trademarks belong to their respective owners.

It builds on Baileys, an unofficial client. Automating a personal messaging account can violate that app's Terms of Service and may get the account restricted or banned. You are solely responsible for how you use this software, for the number(s) you connect, and for complying with all applicable terms and laws. Intended for personal, educational, and development use.

Provided "as is", without warranty of any kind (see LICENSE). The authors are not liable for any damages, bans, or losses arising from its use. This is not legal advice.

πŸ“„ License

MIT β€” see LICENSE. Self-host it, fork it, ship it.

About

πŸ›°οΈ Relay β€” self-hostable bridge between the green chat app 🟒 and HTTP. Inbound messages β†’ webhooks, outbound via one REST call. Multi-connection, SQLite-backed, with a built-in control UI.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors