Skip to content

NATS WebSocket

A Matrix browser tab is a real NATS client. It connects directly to a NATS WebSocket port and speaks NATS protocol. The gateway exposes this connection as a same-origin path — /nats-ws — so the browser does not need a separate domain or absolute URL for its bus connection.

Why same-origin

A browser-side NATS client (the nats.ws library) opens a WebSocket. If that WebSocket points at a different origin (e.g. wss://nats.hivecast.ai/), several things break:

  • CORS becomes relevant. Cookie-based session identity does not flow.
  • The browser separately establishes a TLS handshake and TCP connection.
  • The page's origin policy (CSP) needs additional allowlisting.

Same-origin avoids all of that. The browser opens wss://<page-origin>/nats-ws, the WebSocket reuses the page's connection session, and the gateway proxies internally to the local NATS server.

The bootstrap response always returns:

json
{
  "natsWsUrl": "wss://<page-origin>/nats-ws",
  "transportPlan": {
    "wsPath": "/nats-ws",
    "wsMode": "same-origin-proxy"
  }
}

(See host-platform-http-surface.ts:520-552.)

How the upgrade works

In http-listener.ts:90-93:

typescript
server.on('upgrade', (req, socket, head) => {
  this._handlers.onUpgrade(req, socket, head);
});

The onUpgrade handler — provided by host-http-runtime.ts — checks that the request path is /nats-ws, then proxies the WebSocket handshake to the local NATS server's WebSocket port (typically 4271 for the bundled embedded NATS).

After handshake, every WebSocket frame is forwarded both directions. The gateway is purely a connection relay; it does not parse NATS protocol.

Production topology

In production, Caddy terminates TLS on 443, then proxies /nats-ws to the local gateway on 3100, which proxies the WebSocket upgrade to the NATS WS port:

Browser
  | wss://hivecast.ai/nats-ws
  v
Caddy (443)                 // Caddyfile.bare:2-3
  | reverse_proxy localhost:4223      ← Caddy proxies /nats-ws DIRECTLY to NATS WS port
  v
NATS server (4223)

Note: In the cloud production deployment, Caddy currently proxies /nats-ws directly to NATS port 4223, not through the gateway. The gateway's own /nats-ws upgrade handler exists for local-mode deployments where Caddy isn't involved. Both work; the local-mode path is the universal fallback.

Local topology

Local install (no Caddy):

Browser
  | ws://127.0.0.1:3100/nats-ws
  v
Gateway HTTP listener (3100)
  | upgrade -> proxy
  v
NATS embedded WS port (4271 by default)

The gateway IS the WebSocket terminator in local mode. This is the path exercised by hivecast install + hivecast start developer flows.

NATS authentication

The WebSocket handshake itself does not authenticate. Authentication happens in the NATS protocol's CONNECT frame after the WS is open. The bootstrap response tells the browser which mode:

authModeBrowser sends in CONNECT
'anonymous'No credentials. NATS server has a public-session user with restricted permissions.
'jwt'A JWT fetched from /api/nats-jwt. Bound to the principal's NATS account.

busAuthMode: 'bus-token' adds a layer: the browser also sends a short-lived bus token (returned in bootstrap.busToken) that downstream actors use for authorization checks beyond NATS-level publish/subscribe permissions.

What NATS WebSocket is NOT

  • Not a Matrix-specific protocol. The browser speaks the standard NATS WS protocol; any NATS client works.
  • Not a request/reply translator. Matrix request/reply is layered on top of NATS subjects; it is not a separate protocol over the WS.
  • Not a long-poll fallback. WebSocket is required.

Connection management

Each browser tab opens its own WebSocket. There is no connection pool. When the tab closes, NATS sees a disconnect; the browser's actor mounts go away from system.registry after the heartbeat TTL.

For multi-tab coordination, individual mounts use componentIds scoped to the tab; the registry tracks one provider per (componentId × tab).

See also

Source: projects/matrix-3/packages/system-gateway-http/src/http-listener.ts:90-93, projects/matrix-3/packages/system-gateway-http/src/host-platform-http-surface.ts:520-552, projects/deploy-cloud/Caddyfile.bare.