Consolidates WhatsApp, Signal, Email, and Matrix into a single HTTP control plane. Implements the unified gateway pattern from the OpenClaw study (projects/external/openclaw-study/).
#Architecture
Claude Code / MCP / cURL
│
▼
┌──────────────────────────┐
│ haak-gateway │ Rust, Axum
│ 127.0.0.1:18790 │ data/gateway.db (message log)
│ │ data/contacts.db (channel routing)
└──┬────────┬─────────┬────────┬───┘
│ │ │ │
▼ ▼ ▼ ▼
WhatsApp Signal Email Matrix
(Go bridge (signal-cli (gmail-send (matrix-bridge.py
:8080) CLI) .py) OIDC → matrix.org)
#API
| Method | Path | Purpose |
|---|---|---|
| POST | /send | Send message. Auto-resolves channel from recipient via contacts.db. Override with channel field. |
| POST | /send-file | Send file attachment. Same routing. |
| GET | /status | Health check — probes all adapters, returns connected/error per channel. |
| GET | /history?contact=X&limit=N | Message log from gateway.db. |
| GET | /channels | All contact→channel mappings (5,050 contacts). |
#Send request
{
"recipient": "johannes stelzer",
"message": "text here",
"channel": "whatsapp" // optional — auto-resolved if omitted
}
#Channel resolution
- If
channelspecified → use it - If recipient contains
@→ email - If recipient is all digits (10+) → whatsapp
- Name lookup in contacts.db: phone → whatsapp, matrix_id → matrix, email → email
- Partial match on name (must be unambiguous)
#Adapters
Go bridge (projects/external/whatsapp-mcp/whatsapp-bridge/) on :8080. Two endpoints: POST /api/send (message + media), POST /api/download. Health: TCP probe on 8080.
#Signal
Calls signal-cli binary directly. Requires linked account (QR scan, done). Health: signal-cli listAccounts returns non-empty.
Python subprocess: .claude/scripts/gmail-send.py. OAuth credentials at ~/.googleworkspacemcp/credentials/. Health: credentials directory exists.
#Matrix
Python bridge daemon (.claude/skills/matrix/scripts/matrix-bridge.py). Account: @zach.mainen:matrix.org on matrix.org public homeserver. OIDC device-code auth (matrix.org migrated from password login to MAS in 2025). Bridge stores to data/matrix/messages.db; gateway ingests via 30s poll. Health: config file + DB + launchd daemon status.
#Infrastructure
- Binary:
daemons/gateway/target/release/haak-gateway - Launchd:
com.haak.haak-gateway(KeepAlive) - Log:
~/Library/Logs/haak-gateway.log - Databases:
data/gateway.db(message log),data/contacts.db(routing) - Vault: registered as service
haak-gateway
#Relationship to OpenClaw
The OpenClaw study identified five patterns worth adopting. The gateway addresses the first: unified multi-channel messaging. OpenClaw's gateway is a WebSocket control plane (Node.js, :18789); ours is HTTP/REST (Rust, :18790). Same pattern, different implementation — HTTP is simpler for tool-based agents that don't need persistent connections.
Remaining OpenClaw patterns not yet adopted:
- Live session awareness (sessionslist / sessionssend)
- Agent-scheduled cron (replace launchd with agent-level scheduling)
- Vector memory (semantic retrieval alongside structural navigation)
- A2UI / Canvas (agent-controlled visual workspace)
#Integration plan
#Done
- [x] Gateway built and running (Rust, Axum,
:18790) - [x] WhatsApp adapter wired to Go bridge on
:8080 - [x] Signal adapter via signal-cli (linked account)
- [x] Email adapter via gmail-send.py
- [x] Contact routing from contacts.db (5,050 contacts)
- [x] Message logging to gateway.db
- [x] Launchd daemon (KeepAlive)
- [x] Vault registration
#Next
- [ ] Wire MCP messaging tools through gateway instead of direct bridge calls
- [ ] Telegram adapter (grammY or Telegram Bot API)
- [ ] GCloud sync — replicate gateway.db to VM for remote sessions
- [ ] Vault hook — replace check-services.sh WhatsApp check with gateway /status
- [ ] Populate
matrix_idin contacts.db for contacts who use Matrix - [ ] Entity graph integration —
ingest_comms.pyfor Matrix messages - [ ] Viewer — Matrix messages in Messages tab
#Operational notes — Matrix
Hard-won lessons. Read before touching the Matrix adapter.
OIDC, not passwords. matrix.org runs MAS (Matrix Authentication Service) as of 2025. Password login via the Client API returns M_FORBIDDEN. Auth requires RFC 8628 device-code flow: register a dynamic OIDC client, request a device code, user approves in browser, exchange for access + refresh tokens. Config lives at ~/.config/matrix/config.json.
Token refresh is a race. OIDC access tokens expire in 4 hours. The bridge auto-refreshes on 401. But if you refresh the token externally (e.g., from a CLI script), the bridge's copy is invalidated and it starts getting 401s. After any external token refresh, restart the bridge: launchctl kickstart -k gui/$(id -u)/com.haak.matrix-bridge.
urllib, not aiohttp. Python 3.10's aiohttp has an unfixable brotli decompression bug (ContentEncodingError: Can not decode content-encoding: br). Installing brotli, upgrading aiohttp, installing aiohttp[speedups] — none worked. The bridge uses stdlib urllib with Accept-Encoding: identity to bypass content negotiation entirely.
Sync timeout headroom. The Matrix /sync endpoint long-polls: the server holds the connection for N seconds before responding empty. The urllib timeout must exceed the server-side timeout, or you get RemoteDisconnected / TimeoutError on every cycle. Current: server timeout = POLL_INTERVAL * 1000ms, urllib timeout = that + 15s.
User ID. The account is @zach.mainen:matrix.org (not @zmainen). The whoami endpoint is authoritative; the config file can drift.
DNS in launchd. The bridge occasionally hits transient DNS failures (nodename nor servname provided) in the launchd context even when interactive DNS works fine. The retry loop (sleep 5s, continue) handles this — but if you see sustained DNS errors in the log, restart the daemon.
Strategy 24 — HAAK Gateway — Unified Messaging — 2026 — Zachary F. Mainen / HAAK