@askexenow/exe-gateway
v0.3.19
Published
Standalone webhook server + messaging gateway for multi-platform bot deployment
Downloads
256
Readme
exe-gateway is a self-hosted messaging gateway that connects WhatsApp, Telegram, Discord, Slack, Email, iMessage, Signal, Webchat, and Webhooks behind a single REST API. It handles rate limiting, typing simulation, and multi-account support out of the box — so your messages look human, not automated.
Features
| Feature | Details |
|---------|---------|
| 10 platform adapters | WhatsApp (Baileys), Telegram (Grammy), Discord, Slack, Email (SMTP/IMAP), iMessage, Signal, Webchat, Webhook, CRM |
| Multi-account | Unlimited numbers per platform. Each account gets its own session and rate limiter. |
| Human-like sending | Typing simulation, randomized delays between messages, per-recipient pacing. |
| Anti-ban rate limiting | Per-platform hourly/daily caps tuned to each platform's tolerance. |
| Full history sync | First WhatsApp link pulls complete message history. |
| REST API | Send messages, list groups, check contacts, view rate limits, health checks. |
| One-command install | Single curl command sets up Node.js, clones the repo, builds, configures systemd. |
| Production-ready | Systemd service, nginx config, Docker support, security-hardened. |
| Standalone | Works on its own. Optional hooks for Exe OS integration. |
Quick Start
One-command install (VPS — Ubuntu 22.04+ / Debian 12+)
curl -fsSL https://raw.githubusercontent.com/AskExe/exe-gateway/main/install.sh | bashThis installs Node.js 20, clones the repo to /opt/exe-gateway, builds from source, creates a system user, writes /home/exe/.exe-os/gateway.json, writes /etc/exe-gateway/exe-gateway.env, and installs the systemd service.
Pair WhatsApp
sudo -u exe node /opt/exe-gateway/pair-whatsapp.mjs my-account +1234567890The pairing script reads the same config and proxy settings as the gateway, then stores the session under /home/exe/.exe-os/.auth/.
Start the service
systemctl start exe-gatewayVerify
curl -H "Authorization: Bearer <your-token>" http://127.0.0.1:3100/healthThe raw auth token is shown once during installation; only its SHA-256 hash is stored in /etc/exe-gateway/exe-gateway.env.
Manual install (any platform)
git clone https://github.com/AskExe/exe-gateway.git
cd exe-gateway
npm ci
npm run build
mkdir -p ~/.exe-os
cp deploy/gateway.example.json ~/.exe-os/gateway.json
cp deploy/.env.example .env
set -a && source .env && set +a
node dist/bin/exe-gateway.jsWhatsApp IP Safety
Running on a local machine (laptop, home server, office network)?
You're already on a residential IP — skip this section entirely. Tailscale is not needed. Just install, pair, and go.
WhatsApp actively detects and bans datacenter IP addresses. If you're running exe-gateway on a VPS or cloud server (AWS, DigitalOcean, Hostinger, Hetzner, etc.), you must route WhatsApp traffic through a residential IP.
Why this matters
Connecting WhatsApp directly from a datacenter IP will trigger verification loops or outright bans. This is WhatsApp's anti-automation measure — it flags IPs that belong to known hosting providers.
Solution A: SOCKS proxy (recommended)
Route only WhatsApp traffic through your home machine via a lightweight SOCKS5 proxy over Tailscale. This avoids routing ALL VPS traffic through the exit node, which can break Cloudflare and other services.
VPS (Cloud/Datacenter) Home Machine
┌─────────────────────────┐ ┌─────────────────────────┐
│ exe-gateway │ │ microsocks (SOCKS5) │
│ Baileys + SocksProxy │◄─────────►│ bound to Tailscale IP │
│ (only WhatsApp traffic) │ Tailscale │ (residential IP) │
└─────────────────────────┘ mesh └─────────────────────────┘
↓
WhatsApp servers
see: residential IP1. Install a SOCKS5 proxy on your home machine:
# macOS
brew install microsocks
microsocks -i $(tailscale ip -4) -p 1080 &
# Linux
apt install microsocks
microsocks -i $(tailscale ip -4) -p 1080 &2. Configure exe-gateway to use the proxy:
# In /etc/exe-gateway/exe-gateway.env
WHATSAPP_PROXY_URL=socks5://<home-tailscale-ip>:10803. Verify:
# From VPS — should show your home IP
curl --socks5-hostname <home-tailscale-ip>:1080 https://ifconfig.meSolution B: Tailscale exit node (simpler but routes all traffic)
Route ALL VPS traffic through a home machine using Tailscale's exit node feature. Simpler setup but can interfere with Cloudflare, nginx, and other services on the VPS.
# Home machine
tailscale up --advertise-exit-node
# VPS
tailscale set --exit-node=<home-machine-name>
# Verify
curl -s ifconfig.me # Should show HOME IPWarning: Exit node routes all traffic, including return paths for inbound connections. If your VPS serves websites behind Cloudflare or a reverse proxy, use Solution A instead.
See docs/tailscale-exit-node.md for troubleshooting, DNS issues, firewall config, and keeping the exit node online.
Multi-Account Configuration
Runtime config lives at ~/.exe-os/gateway.json by default, or the path set in EXE_GATEWAY_CONFIG. Secrets and environment overrides live in /etc/exe-gateway/exe-gateway.env in the installer flow.
For PostgreSQL you can either set DATABASE_URL=postgresql://user:pass@host:5432/dbname or use the split EXE_GATEWAY_DB_* variables from deploy/.env.example.
{
"port": 3100,
"whatsappVerifyToken": "",
"adapters": {
"whatsapp": {
"enabled": true,
"credentials": {
"app_secret": "set-if-you-use-meta-whatsapp-webhooks"
},
"accounts": [
{
"name": "sales",
"authDir": "/home/exe/.exe-os/.auth/whatsapp-sales"
},
{
"name": "support",
"authDir": "/home/exe/.exe-os/.auth/whatsapp-support"
}
]
},
"telegram": {
"enabled": true,
"accounts": [
{
"name": "main-bot",
"bot_token": "123456:ABC-DEF...",
"secret_token": "set-if-you-use-telegram-webhooks"
}
]
},
"discord": {
"enabled": true,
"accounts": [
{
"name": "community-bot",
"bot_token": "your-discord-token",
"public_key": "set-if-you-use-discord-interactions"
}
]
},
"slack": {
"enabled": true,
"accounts": [
{
"name": "workspace-bot",
"bot_token": "xoxb-...",
"app_token": "xapp-..."
}
]
},
"email": {
"enabled": true,
"accounts": [
{
"name": "notifications",
"smtp_host": "smtp.example.com",
"smtp_port": "587",
"smtp_user": "[email protected]",
"smtp_pass": "your-password",
"from_address": "[email protected]"
}
]
}
}
}Admin API bearer tokens are hashed at rest. Save the raw token once during install; the installer persists only EXE_GATEWAY_AUTH_TOKEN_HASH in /etc/exe-gateway/exe-gateway.env.
If you expose inbound provider webhooks, configure the matching signature secret in gateway.json:
- WhatsApp (Meta Cloud API):
adapters.whatsapp.credentials.app_secret→ validatesX-Hub-Signature-256 - Telegram:
accounts[].secret_token(or adapter-levelsecret_token) → validatesX-Telegram-Bot-Api-Secret-Token - Discord interactions/webhooks:
accounts[].public_key(or adapter-levelpublic_key) → validates the Ed25519 signature headers
Pair each WhatsApp account separately:
sudo -u exe node /opt/exe-gateway/pair-whatsapp.mjs sales +1234567890
sudo -u exe node /opt/exe-gateway/pair-whatsapp.mjs support +0987654321API Reference
/api/* and /v1/usage/* require the Authorization: Bearer <token> header (except /health).
Provider webhook routes use provider-specific signature validation instead of the admin bearer token.
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /health | Server health, uptime, registered platforms |
| POST | /api/send | Send a message (rate-limited, with typing simulation) |
| GET | /api/groups | List WhatsApp groups |
| GET | /api/group/:id | Group metadata + participants |
| GET | /api/limits | Rate limit stats per platform |
| POST | /webhook/:platform | Incoming webhook payload from external platform |
Send a message
curl -X POST http://127.0.0.1:3100/api/send \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"platform": "whatsapp",
"account": "sales",
"to": "+1234567890",
"text": "Hey — just following up on our conversation."
}'The message enters the outbound queue and is sent with realistic typing simulation and randomized delay. You don't need to manage pacing — the limiter handles it.
List groups
curl -H "Authorization: Bearer <token>" http://127.0.0.1:3100/api/groupsCheck rate limits
curl -H "Authorization: Bearer <token>" http://127.0.0.1:3100/api/limitsRate Limiting
Outbound messages are paced per-platform with human-like timing. These are the defaults — tuned to avoid platform bans:
| Platform | Per Hour | Per Day | Typing Simulation | Delay Between Messages | |----------|----------|---------|-------------------|----------------------| | WhatsApp | 30 | 200 | 1.5–8s (25 cps) | 5–15s per recipient | | Telegram | 60 | 500 | 1–5s (40 cps) | 2–8s per recipient | | Discord | 120 | 1,000 | 0.5–4s (50 cps) | 1–5s per recipient | | Slack | 120 | 1,000 | 0.5–4s (50 cps) | 1–5s per recipient | | Email | 20 | 100 | None | 10–30s per recipient |
Inbound messages are also rate-limited: 10 req/s per sender, 100 req/s global (sliding window).
Teddy Memory Scope
Teddy is read-only by default. You can further restrict which agent memories Teddy can read:
# Empty = all readable memory. Example below restricts memory reads to Mari/CMO only.
EXE_GATEWAY_TEDDY_MEMORY_AGENTS=mariThis is enforced in Teddy's tool handlers, not just the prompt. Requests for non-allowed agents return an access denied message.
Automated Outbound Guardrail
Automated outbound is deny-by-default. A fresh install can ingest and store WhatsApp messages, but it will not send bot responses, rate-limit notices, or auto-replies until an explicit response scope is configured.
There are three separate permission categories:
- DM sender — respond to this person only in direct messages.
- Whole group — respond to any allowed message in a specific group.
- Specific person in group — respond only when a specific number/JID speaks inside a group.
# empty = ingest-only, no automated replies
EXE_GATEWAY_RESPONSE_ALLOW_DMS=
EXE_GATEWAY_RESPONSE_ALLOW_GROUPS=
EXE_GATEWAY_RESPONSE_ALLOW_GROUP_PARTICIPANTS=
# DM only with one person
EXE_GATEWAY_RESPONSE_ALLOW_DMS=+16179354486,[email protected]
# Whole group by group JID
[email protected]
# Specific person in any group, or scoped to one group
EXE_GATEWAY_RESPONSE_ALLOW_GROUP_PARTICIPANTS=+16179354486
EXE_GATEWAY_RESPONSE_ALLOW_GROUP_PARTICIPANTS=120363428671509944@g.us:+16179354486
# Optional group command gate: even allowlisted groups/participants only get replies
# when the message starts with /ted or /ted<space>. DMs are unaffected.
EXE_GATEWAY_RESPONSE_GROUP_PREFIX=/ted
# Optional group stop command: allowed group speakers can send /stop to suppress
# an in-flight/stale group response. Default in code: /stop. Empty disables.
EXE_GATEWAY_RESPONSE_GROUP_STOP_COMMAND=/stop
# Legacy DM alias still supported
EXE_GATEWAY_RESPONSE_ALLOW_CONTACTS=+16179354486
# Emergency switch: true ignores all group response scopes
EXE_GATEWAY_RESPONSE_DM_ONLY=falseA DM allowlist never grants group response permission. This guard applies even if a bot is registered as the default route. It exists to prevent a newly linked WhatsApp account from unexpectedly messaging contacts or groups.
Teddy delegated sends
Teddy can be allowed to send WhatsApp text messages on Henry/operator's behalf from an approved DM. This is not auto-response: Teddy only sends when the approved operator explicitly asks it to send a specific message to a specific chat/contact.
# Empty = disabled. If unset, Teddy falls back to EXE_GATEWAY_RESPONSE_ALLOW_DMS.
EXE_GATEWAY_OPERATOR_SEND_ALLOW_DMS=+16179354486Guardrails:
- only approved direct-message senders get this write permission;
- group prompts never grant delegated send permission;
- Teddy can only use the
send_whatsapp_messagetool, not Exe OS task/memory writes; - recipient is resolved from stored WhatsApp groups/contacts, or an explicit JID/phone.
Operator notifications for contact segments
Operator notifications are separate from auto-response. They do not reply to the sender. They send a private alert to configured operator DMs when a watched contact messages the gateway.
This supports local/operator-defined contact segments using a suffix convention such as (Alpha), (VIP), or (Agency). The segment is not hardcoded; it is configured per deployment.
# OFF by default
EXE_GATEWAY_CONTACT_SEGMENT_NOTIFY_ENABLED=false
# When enabled, contacts whose alias/saved/local/display/push/profile name ends
# with this suffix trigger an operator notification.
EXE_GATEWAY_CONTACT_SEGMENT_NOTIFY_NAME_SUFFIX="(Alpha)"
# Send the alert to these operator DMs.
EXE_GATEWAY_CONTACT_SEGMENT_NOTIFY_RECIPIENTS=+16179354486
# Default false: only notify for DMs to the gateway, not group chatter.
EXE_GATEWAY_CONTACT_SEGMENT_NOTIFY_INCLUDE_GROUPS=falseAuto-Reply
Automatic replies for incoming messages — disabled by default, allowlist-gated, with 8 safety gates to prevent spam. Auto-reply allowContacts / allowGroups also opt those exact senders into the global automated outbound allowlist.
Add to gateway.json:
{
"autoReply": {
"enabled": true,
"message": "Received. We'll get back to you shortly.",
"allowGroups": ["[email protected]"],
"allowContacts": ["+16179354486"],
"cooldownHours": 24,
"dailyCap": 20,
"dmOnly": false
}
}| Setting | Default | Description |
|---------|---------|-------------|
| enabled | false | Master switch. No replies sent unless explicitly true. |
| message | "Received." | Text to send as the auto-reply. |
| allowGroups | [] | Only reply in these group JIDs. Empty = no group replies. |
| allowContacts | [] | Only reply to these phone numbers/JIDs. Empty = no DM replies. |
| cooldownHours | 24 | Minimum hours between replies to the same contact. |
| dailyCap | 20 | Maximum total auto-replies per day across all contacts. |
| dmOnly | false | If true, blocks all group replies regardless of allowGroups. |
Safety gates (always enforced, not configurable):
- Must be explicitly enabled (default OFF)
- Never replies to historical/sync messages
- Never replies to your own messages
- Never replies to empty or system messages
- Never replies to read receipts, reactions, or calls
- Only replies to allowlisted groups or contacts (must have at least one allowlist)
- Per-contact cooldown (default 24h)
- Daily cap (default 20)
Auto-replies include a random 3–15 second delay and typing indicator simulation to appear human.
Read-Only Mode
Background conversation monitoring — receives and stores all messages with zero bot footprint. No auto-reply, no typing indicators, no read receipts, no outbound sends.
Add to gateway.json:
{
"readOnly": true,
"database": { "host": "...", "port": 5432, "user": "...", "password": "...", "database": "..." }
}What happens in read-only mode:
| Behavior | Status |
|----------|--------|
| Receive messages | ✅ Normal |
| Store to PostgreSQL | ✅ Normal |
| Pipeline ingest (CRM, contacts) | ✅ Normal |
| History sync | ✅ Normal |
| Auto-reply | 🔴 Force disabled |
| Bot responses | 🔴 Suppressed |
| Typing indicators | 🔴 Suppressed |
| POST /api/send | 🔴 Rejected (403) |
| Online presence | 🔴 Already off (markOnlineOnConnect: false) |
The /health endpoint shows "mode": "read-only" when active.
Combine with storageFilter to selectively ingest only specific groups/contacts.
Data Ingestion Adapters
exe-gateway serves as the data ingestion layer for the Exe platform. It runs API adapters (cron or webhook) that pull data from clients' external systems and stage it for routing into the wiki and CRM.
How it works
External APIs → exe-gateway adapters → staging.raw_imports → (routing handled downstream)The gateway's job is to pull and stage — it does not route data into wiki or CRM schemas. Routing happens in a separate transform step after staging.
Adapter lifecycle
- Adapter runs on a configurable schedule (cron: 15min / hourly / daily)
- Checks
staging.sync_cursorsfor the last pull position per source - Pulls new/updated records from the external API
- Writes raw JSON to
staging.raw_imports(same Postgres instance,stagingschema) - Updates
sync_cursorswith the new position
For platforms that support it (Stripe, Asana), webhooks provide real-time ingestion alongside cron-based pulls.
Supported adapters (current / planned)
| Adapter | Protocol | Status | |---------|----------|--------| | Xero | OAuth 2.0 REST | Planned | | Stripe | REST + Webhooks | Planned | | Asana | REST + Webhooks | Planned | | Banking APIs | Open Banking REST | Planned | | QuickBooks | OAuth 2.0 REST | Planned |
Configuration
Each adapter is configured via environment variables:
# Example: Xero adapter
XERO_CLIENT_ID=your-client-id
XERO_CLIENT_SECRET=your-client-secret
XERO_TENANT_ID=your-tenant-id
XERO_SYNC_INTERVAL=hourly # 15min | hourly | dailyRelated repos
- exe-wiki/ARCHITECTURE.md — Full staging/routing architecture, schema definitions, routing rules
- exe-crm — CRM-side entity mapping and how routed data lands in contacts/deals/activities
Integration with Exe OS (Optional)
exe-gateway is fully standalone. To integrate with Exe OS for memory, wiki, and CRM hooks:
import { setHooks } from "@askexenow/exe-gateway";
import { orgBus } from "@askexenow/exe-os/dist/lib/state-bus.js";
import { ingest } from "@askexenow/exe-os/dist/lib/pipeline-router.js";
setHooks({
onEvent: (event) => orgBus.emit(event),
onIngest: (msg) => ingest(msg),
});This pipes all incoming messages through the Exe OS memory pipeline and broadcasts events to the organization bus.
Deployment
Systemd (recommended for VPS)
The installer sets this up automatically. To configure manually:
cp exe-gateway.service /etc/systemd/system/
mkdir -p /etc/exe-gateway
cp deploy/.env.example /etc/exe-gateway/exe-gateway.env
systemctl daemon-reload
systemctl enable --now exe-gatewayView logs:
journalctl -u exe-gateway -fThe service runs as a dedicated exe system user with security hardening. Tune NODE_OPTIONS and other secrets in /etc/exe-gateway/exe-gateway.env.
Nginx reverse proxy
For SSL termination and public-facing deployments:
cp nginx-gateway.conf /etc/nginx/sites-available/gateway.example.com
ln -s /etc/nginx/sites-available/gateway.example.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginxReplace the example domain and certificate paths first. The template now proxies /health, /webhook/*, /api/*, and /v1/*, and includes an optional /ws block for the WebSocket relay.
Docker
docker build -t exe-gateway .
cp deploy/gateway.example.json ./gateway-data/gateway.json
# Copy deploy/.env.example to your own env file and replace EXE_GATEWAY_AUTH_TOKEN first.
docker run -d \
--name exe-gateway \
-p 3100:3100 \
-p 3101:3101 \
--env-file ./gateway.env \
-v ./gateway-data:/data \
exe-gatewayWhatsApp v7 upgrade: QR re-link required
exe-gateway uses Baileys v7 for WhatsApp linked-device support. Baileys v7 is a breaking auth-state migration from 6.x. If you upgrade an existing WhatsApp-connected gateway, you must re-link each WhatsApp account once by scanning a new QR code.
Why: WhatsApp/Baileys changed the linked-device session format. Reusing old 6.x auth state can break receive/decrypt, especially for group messages.
Migration steps:
- Back up the current WhatsApp auth directory.
- Reset the account auth directory.
- Restart exe-gateway.
- Open
https://<gateway-domain>/pair/default?token=<admin-token>. - Scan from WhatsApp → Linked Devices.
- Verify direct receive, group receive, send, and history sync.
Installer users: if existing WhatsApp auth is detected, the installer stops instead of silently breaking the account. To intentionally back up and reset auth for v7, rerun with:
EXE_GATEWAY_CONFIRM_BAILEYS_V7_REPAIR=1 bash install.shNo conversation data is deleted. Expect 2-5 minutes of WhatsApp downtime per account while re-linking. See docs/BAILEYS_V7_MIGRATION.md for the full runbook.
Platform Adapters
| Platform | Library | Status | Notes | |----------|---------|--------|-------| | WhatsApp | Baileys (Web protocol) | Production | Multi-account, full history sync, QR pairing | | Telegram | Grammy | Production | Bot API, inline keyboards, media | | Discord | discord.js | Production | Slash commands, threads, embeds | | Slack | Bolt + Web API | Production | Socket Mode, interactive messages | | Email | Nodemailer + IMAP | Production | SMTP outbound, IMAP inbound | | iMessage | macOS native | Beta | Requires macOS host | | Signal | signal-cli | Beta | Requires signal-cli daemon | | Webchat | WebSocket | Production | Browser widget, real-time | | Webhook | Generic HTTP | Production | Any platform via HTTP POST | | CRM | Exe CRM bridge | Production | Bi-directional contact/deal sync |
Contributing
- Fork the repo
- Create a feature branch (
git checkout -b feat/my-feature) - Make your changes with tests
- Run
npm testandnpm run typecheck - Open a PR
