Appearance
WebSocket
The browser-side adapter. Browsers do not speak raw TCP NATS; they speak WebSocket. Matrix exposes a same-origin /nats-ws endpoint through the HTTP gateway that upgrades to a WebSocket and proxies to the local NATS server. The browser then runs a normal NATS client on top.
The same-origin rule
Source:
CLAUDE.mdRule 7. Quoted: "Browser connects via same-origin WebSocket. Always/nats-wson the SAME origin. Never absolute WebSocket URLs from bootstrap."
This rule is non-negotiable. Reasons:
- Cookie scope. The session cookie (
mx_session) is bound to the origin. Cross-origin WebSocket can't carry the cookie reliably. - CSP / mixed content. A page loaded from
http://localhost:3100cannot WebSocket-connect towss://hivecast.aiwithout explicit CSP and CORS configuration that few deployments get right. - Federation locality. The local Host's NATS, plus its leaf to a hub, is the right place for routing. Bypassing the gateway and connecting directly to a hub bypasses local mounts.
In every browser bootstrap, the connection URL is computed:
typescript
const wsUrl = new URL('/nats-ws', window.location.origin);
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';Always relative; never hard-coded.
How the upgrade works
Browser → HTTP gateway (port 3100)
GET /nats-ws Upgrade: websocket
→ gateway authenticates session via cookie
→ gateway mints a NATS-scoped JWT for the user
→ gateway opens a NATS WebSocket connection on behalf of the browser
→ bidirectional bytes flow: browser ↔ gateway ↔ NATSFrom the browser's NATS client perspective this is a normal WebSocket-mode NATS connection. From the gateway's perspective it is a proxy with authentication interception.
JWT scoping
The gateway issues short-lived NATS JWTs with subject permissions pinned to the principal's authority root:
| Subject | Permission |
|---|---|
{authorityRoot}.> | publish, subscribe |
{authorityRoot}.$reply.> | subscribe |
| any other root | denied |
The browser cannot escape its root by manipulating subjects — NATS rejects the publish at the wire level.
Reconnection
Browser tabs that lose the WebSocket reconnect automatically through the NATS client's built-in reconnect logic. On reconnect, all of the tab's subscriptions re-attach. State the actor maintained in JetStream KV survives; in-flight requests that timed out are not replayed.
There is a known gap (see WORKSTREAMS/matrix-web/IMPLEMENTATION-MAP.md Hole 62) where the JWT may need refresh on long-lived tabs. The current implementation schedules a JWT refresh timer — see projects/matrix-3/packages/core/src/transport/createBrowserNatsTransport.ts.
Browser-tab as runtime
Each browser tab registers as a runtime in system.runtimes:
runtimeId: RUNTIME-BROWSER-F81354A7 (random per tab)
type: 'browser'
sessionMount: ''Director and the catalog see browser tabs alongside server-side runtimes. Tabs that close fire runtimes.deregister; if a tab crashes without deregistering, the runtime record times out.
Cookies on the WebSocket
The browser includes the session cookie in the WebSocket Upgrade request automatically (same-origin). The gateway reads it from the incoming upgrade request, validates via auth.session.validate, and binds the resulting principal to the proxied connection. The browser NATS client then operates under that identity for the connection's lifetime.
If the session expires while the connection is open, the gateway closes the WebSocket — the browser sees a disconnect and falls back to its reconnect logic, which will redirect to login if the cookie is dead.
What WebSocket is NOT used for
- It is NOT used for the actor↔actor wire. Server-side actors talk to NATS over TCP (or the in-memory transport in tests). WebSocket is the browser entry only.
- It is NOT used for federation between Hosts. Cross-Host federation uses NATS leaf nodes (also outbound, but not WebSocket — TCP-leaf to port 7422).
- It is NOT a generic WebSocket API for arbitrary endpoints. The gateway exposes only
/nats-ws. Custom WebSocket endpoints are not supported by the gateway today.
See also
- NATS — what
/nats-wsproxies to. - HTTP gateway — the Host that owns
/nats-ws. - Principal identity — cookie-derived identity for the upgrade.
CLAUDE.md— Rule 7.