{
  "title": "Gateway architecture",
  "content": "Last updated: 2026-01-22\n\n* A single long‑lived **Gateway** owns all messaging surfaces (WhatsApp via\n  Baileys, Telegram via grammY, Slack, Discord, Signal, iMessage, WebChat).\n* Control-plane clients (macOS app, CLI, web UI, automations) connect to the\n  Gateway over **WebSocket** on the configured bind host (default\n  `127.0.0.1:18789`).\n* **Nodes** (macOS/iOS/Android/headless) also connect over **WebSocket**, but\n  declare `role: node` with explicit caps/commands.\n* One Gateway per host; it is the only place that opens a WhatsApp session.\n* A **canvas host** (default `18793`) serves agent‑editable HTML and A2UI.\n\n## Components and flows\n\n* Maintains provider connections.\n* Exposes a typed WS API (requests, responses, server‑push events).\n* Validates inbound frames against JSON Schema.\n* Emits events like `agent`, `chat`, `presence`, `health`, `heartbeat`, `cron`.\n\n### Clients (mac app / CLI / web admin)\n\n* One WS connection per client.\n* Send requests (`health`, `status`, `send`, `agent`, `system-presence`).\n* Subscribe to events (`tick`, `agent`, `presence`, `shutdown`).\n\n### Nodes (macOS / iOS / Android / headless)\n\n* Connect to the **same WS server** with `role: node`.\n* Provide a device identity in `connect`; pairing is **device‑based** (role `node`) and\n  approval lives in the device pairing store.\n* Expose commands like `canvas.*`, `camera.*`, `screen.record`, `location.get`.\n\n* [Gateway protocol](/gateway/protocol)\n\n* Static UI that uses the Gateway WS API for chat history and sends.\n* In remote setups, connects through the same SSH/Tailscale tunnel as other\n  clients.\n\n## Connection lifecycle (single client)\n\n## Wire protocol (summary)\n\n* Transport: WebSocket, text frames with JSON payloads.\n* First frame **must** be `connect`.\n* After handshake:\n  * Requests: `{type:\"req\", id, method, params}` → `{type:\"res\", id, ok, payload|error}`\n  * Events: `{type:\"event\", event, payload, seq?, stateVersion?}`\n* If `OPENCLAW_GATEWAY_TOKEN` (or `--token`) is set, `connect.params.auth.token`\n  must match or the socket closes.\n* Idempotency keys are required for side‑effecting methods (`send`, `agent`) to\n  safely retry; the server keeps a short‑lived dedupe cache.\n* Nodes must include `role: \"node\"` plus caps/commands/permissions in `connect`.\n\n## Pairing + local trust\n\n* All WS clients (operators + nodes) include a **device identity** on `connect`.\n* New device IDs require pairing approval; the Gateway issues a **device token**\n  for subsequent connects.\n* **Local** connects (loopback or the gateway host’s own tailnet address) can be\n  auto‑approved to keep same‑host UX smooth.\n* **Non‑local** connects must sign the `connect.challenge` nonce and require\n  explicit approval.\n* Gateway auth (`gateway.auth.*`) still applies to **all** connections, local or\n  remote.\n\nDetails: [Gateway protocol](/gateway/protocol), [Pairing](/start/pairing),\n[Security](/gateway/security).\n\n## Protocol typing and codegen\n\n* TypeBox schemas define the protocol.\n* JSON Schema is generated from those schemas.\n* Swift models are generated from the JSON Schema.\n\n* Preferred: Tailscale or VPN.\n* Alternative: SSH tunnel\n  \n* The same handshake + auth token apply over the tunnel.\n* TLS + optional pinning can be enabled for WS in remote setups.\n\n## Operations snapshot\n\n* Start: `openclaw gateway` (foreground, logs to stdout).\n* Health: `health` over WS (also included in `hello-ok`).\n* Supervision: launchd/systemd for auto‑restart.\n\n* Exactly one Gateway controls a single Baileys session per host.\n* Handshake is mandatory; any non‑JSON or non‑connect first frame is a hard close.\n* Events are not replayed; clients must refresh on gaps.",
  "code_samples": [
    {
      "code": "Client                    Gateway\n  |                          |\n  |---- req:connect -------->|\n  |<------ res (ok) ---------|   (or res error + close)\n  |   (payload=hello-ok carries snapshot: presence + health)\n  |                          |\n  |<------ event:presence ---|\n  |<------ event:tick -------|\n  |                          |\n  |------- req:agent ------->|\n  |<------ res:agent --------|   (ack: {runId,status:\"accepted\"})\n  |<------ event:agent ------|   (streaming)\n  |<------ res:agent --------|   (final: {runId,status,summary})\n  |                          |",
      "language": "unknown"
    }
  ],
  "headings": [
    {
      "level": "h2",
      "text": "Overview",
      "id": "overview"
    },
    {
      "level": "h2",
      "text": "Components and flows",
      "id": "components-and-flows"
    },
    {
      "level": "h3",
      "text": "Gateway (daemon)",
      "id": "gateway-(daemon)"
    },
    {
      "level": "h3",
      "text": "Clients (mac app / CLI / web admin)",
      "id": "clients-(mac-app-/-cli-/-web-admin)"
    },
    {
      "level": "h3",
      "text": "Nodes (macOS / iOS / Android / headless)",
      "id": "nodes-(macos-/-ios-/-android-/-headless)"
    },
    {
      "level": "h3",
      "text": "WebChat",
      "id": "webchat"
    },
    {
      "level": "h2",
      "text": "Connection lifecycle (single client)",
      "id": "connection-lifecycle-(single-client)"
    },
    {
      "level": "h2",
      "text": "Wire protocol (summary)",
      "id": "wire-protocol-(summary)"
    },
    {
      "level": "h2",
      "text": "Pairing + local trust",
      "id": "pairing-+-local-trust"
    },
    {
      "level": "h2",
      "text": "Protocol typing and codegen",
      "id": "protocol-typing-and-codegen"
    },
    {
      "level": "h2",
      "text": "Remote access",
      "id": "remote-access"
    },
    {
      "level": "h2",
      "text": "Operations snapshot",
      "id": "operations-snapshot"
    },
    {
      "level": "h2",
      "text": "Invariants",
      "id": "invariants"
    }
  ],
  "url": "llms-txt#gateway-architecture",
  "links": []
}