Skip to content

Browser transport

createBrowserNatsTransport is the browser-side path. It opens a WebSocket to a NATS server using nats.ws, optionally negotiates JWTs against a token endpoint, and returns a NatsTransport ready to plug into a MatrixRuntime. This page documents the function, its three auth modes, and the same-origin contract that makes it work behind any gateway.

Function signature

ts
import { createBrowserNatsTransport } from '@open-matrix/core';

interface ICreateBrowserNatsTransportConfig {
  root?: string;                                          // authority root (required)
  realm?: string;                                          // deprecated alias for root
  wsUrl: string;                                           // WebSocket URL (required)
  token?: string;
  jwtProvider?: () => Promise<{ jwt: string; seed: string }>;
  user?: string;
  pass?: string;
  debug?: boolean;
  connectionTimeoutMs?: number;
  wsFactory?: WsSocketFactory;
}

const transport = await createBrowserNatsTransport(config);

The function is async — it awaits the WebSocket handshake and (if jwtProvider is set) the initial credential fetch. Returns a NatsTransport instance.

The same-origin contract

Warning: Always use wsUrl: '/nats-ws' (relative). Never absolute URLs. The browser must connect to the same origin that served the page.

The platform shells (matrix-web, matrix-edge) and system-gateway-http are configured so that /nats-ws on whatever HTTP port served the page proxies to the local NATS sibling. This is one of the project-level rules in the root CLAUDE.md § "THE RULES" #7:

Browser connects via same-origin WebSocket. Always /nats-ws on the SAME origin. Never absolute WebSocket URLs from bootstrap.

Absolute URLs hard-code the assumption that NATS lives at a particular hostname. They break port-forwarded dev sessions, tunnelled cloud setups, and any scenario where the gateway is behind a reverse proxy.

The three auth modes

createBrowserNatsTransport picks one auth mode based on which fields are set. Order of preference (createBrowserNatsTransport.ts:48-83):

Mode 1 — JWT (operator/account mode)

ts
const transport = await createBrowserNatsTransport({
  root: 'COM.NIMBLETEC.RICHARD-SANTOMAURO',
  wsUrl: '/nats-ws',
  jwtProvider: async () => fetch('/api/nats-jwt').then(r => r.json()),
});

When jwtProvider is set:

  1. The function calls the provider to get an initial { jwt, seed }.
  2. Encodes the seed into bytes via TextEncoder.
  3. Sets up a timer that re-fetches every 4 minutes (JWT validity is 5 minutes; :65-69).
  4. Connects with jwtAuthenticator from @nats-io/nats-core — synchronous closures that read the latest cached values.

The timer is attached to the transport so disconnect() can clear it. JWT refresh failures are logged but do not tear down the connection — the existing JWT remains valid for the rest of its window.

Mode 2 — user/pass (static credentials)

ts
const transport = await createBrowserNatsTransport({
  root, wsUrl: '/nats-ws',
  user: 'no_auth_user',
  pass: '',
});

Used by the auth.mode: public-session path during early bootstrap when the browser hasn't paired yet. After login, the bootstrap re-creates the transport with jwtProvider.

Mode 3 — token

ts
const transport = await createBrowserNatsTransport({
  root, wsUrl: '/nats-ws',
  token: '<bearer>',
});

Used in dev/test setups where NATS is configured with a static authentication token. Not the production path.

If none of jwtProvider, user/pass, or token are set, the function passes empty auth options — NATS rejects the connection unless its server config allows anonymous connections.

Connection options

ts
{
  servers: [wsUrl],
  name: `matrix-browser-${root}`,
  timeout: connectionTimeoutMs ?? 5000,
  reconnect: true,
  maxReconnectAttempts: -1,    // infinite
  wsFactory,                    // optional custom WebSocket constructor
  ...authOpts,
}

reconnect: true and maxReconnectAttempts: -1 mean the browser transport will retry forever in the face of network drops. This matches what users expect — closing a laptop and reopening it eventually reconnects without a page reload.

wsFactory is for environments that need a custom WebSocket (cordova, embedded shells, polyfills). Most browsers use the built-in.

Error handling

If wsconnect throws, the function:

  1. Clears any JWT-refresh timer it had started.
  2. Wraps the original error: createBrowserNatsTransport: failed to connect WebSocket at <wsUrl>: <message>.
  3. Re-throws.

Catch this in your bootstrap to render a connection-failure UI before the rest of the page mounts.

Worked example: full bootstrap

ts
// src/browser/bootstrap.ts
import {
  createBrowserNatsTransport,
  MatrixRuntime,
  BrowserDomStrategy,
} from '@open-matrix/core';

async function main() {
  // 1. Resolve the authority root for this user/session.
  const whoami = await fetch('/api/whoami').then(r => r.json());

  // 2. Build the transport.
  const transport = await createBrowserNatsTransport({
    root: whoami.authorityRoot,
    wsUrl: '/nats-ws',
    jwtProvider: async () => fetch('/api/nats-jwt').then(r => r.json()),
  }).catch(err => {
    document.body.innerHTML = `<pre>Connection failed: ${err.message}</pre>`;
    throw err;
  });

  // 3. Build the runtime.
  const runtime = new MatrixRuntime({
    transport,
    domStrategy: new BrowserDomStrategy(),
  });

  // 4. Mount UI components into a <matrix-dsl-host> root.
  // (Package-specific from here on.)
}

main();

This shape is what every browser-served Matrix package's bootstrapEntry looks like. The exact identity-resolution path (/api/whoami, /api/nats-jwt) is provided by system-gateway-http.

Disconnecting

transport.disconnect() clears the JWT-refresh timer (if any), unsubscribes everything, drains the NATS connection, and closes the WebSocket. Call it in any "page unload" path you control. Modern browsers also tear down WebSockets on page navigation automatically; the explicit call exists for deterministic test cleanup.

See also

Source: projects/matrix-3/packages/core/src/transport/createBrowserNatsTransport.ts:1-112 (full file). Same-origin rule from the root CLAUDE.md § "THE RULES" #7.