Skip to content

What the gateway does

The gateway has exactly five responsibilities, all of which sit on the substrate's closed list of allowed HTTP boundaries (P1.43). Anything outside this list is either a documented projection for non-bus-aware external clients or a bug.

1. Accept HTTP and WebSocket on a single port

The gateway binds one TCP socket (host.http.port, default 3100). Every HTTP request and WebSocket upgrade lands there. Implementation: MatrixHostHttpGatewayServer in projects/matrix-3/packages/system-gateway-http/src/http-listener.ts.

typescript
// http-listener.ts
const server = http.createServer((req, res) => {
  const url = new URL(req.url ?? '/', 'http://matrix-host.local');
  this._dispatch({ req, res, url }).catch((err: unknown) => {
    this._handlers.onError({ req, res, url }, err);
  });
});
server.on('upgrade', (req, socket, head) => {
  this._handlers.onUpgrade(req, socket, head);
});

2. Resolve URLs into Matrix routes

Every non-trivial URL goes through a route grammar. The dispatch order is fixed in _dispatch; earlier matches win, so reserved paths (/healthz, /api/identity/*, /nats-ws) cannot be claimed by an app.

/healthz
/api/identity/bootstrap, /api/identity/nats-creds   ← boundary credentials
/oauth/<provider>/callback                          ← OAuth boundary
/nats-ws                                            ← WebSocket upgrade
/apps/<appName>/<asset-path>
/<spacePath>/<appName>/<asset-path>                 ← public Space route
/<publicNamespace>/<appName>/<asset-path>           ← typed namespace route
/<email>/<appName>/<asset-path>                     ← explicit authority route
/branding/*, /favicon.ico, /manifest.json

Vocabulary note. "Space path" is the canonical name for what older code calls routeKey. New docs and UI copy say Space path; the field name routeKey survives only as a v1 compatibility alias.

3. Serve static assets from the right runtime

For /apps/<appName>/... and the public-route variants, the gateway asks system.gateway.http "which runtime owns this appName?" then asks that runtime's <appName>.http actor (a MatrixHttpAssetEndpointActor) for the bytes via the http.asset.request op.

The runtime that owns the package reads its own dist/browser/ directory. The gateway never opens those files itself; it delegates over the bus.

This is what makes the substrate's package boundary honest: each package owns its own assets, and the gateway is purely a routing/relay layer.

4. Bridge to NATS via WebSocket

GET /nats-ws is upgraded to a WebSocket and proxied to the local NATS WebSocket port. Browsers connect to wss://<host>/nats-ws and speak the NATS WebSocket protocol; messages reach the local NATS server. This is what makes "the browser is a Matrix client" possible without a custom protocol.

5. Mint bootstrap credentials and handle OAuth callbacks

Two narrow purposes:

EndpointPurpose
GET /api/identity/bootstrapReturns the browser-shaped session state (root, principal, NATS WebSocket URL, transport plan). The browser cannot use the bus before it has credentials; this is the boundary that gives them.
GET /api/identity/nats-credsMints NATS user JWTs scoped to the authenticated principal.
GET /oauth/<provider>/callbackOAuth providers redirect to HTTP URLs by protocol; this is unavoidable.
GET /healthzLiveness for external monitors that cannot speak NATS.
POST /webhooks/<provider>Third-party providers POST over HTTP; unavoidable.

These are first-class boundary endpoints. They exist because the boundary exists, not as a substitute for bus actors.

What it does NOT do

The gateway is not, and must not become, any of the following:

  • A TLS terminator. Caddy (or any reverse proxy) does that.
  • An OAuth issuer. That is system.auth. The gateway only handles the HTTP-side redirect/callback; the auth flow itself runs on the bus.
  • A package server. Each package's own runtime serves its own assets; the gateway only routes.
  • An inventory API. Inventory lives on the bus:
    • "What apps are running?" → system.catalog.
    • "What devices are linked?" → system.devices.
    • "What runtimes are alive?" → system.runtimes.
    • "Which mounts are claimed?" → system.registry.
    • "Which AI providers are configured?" → system.inference. Any HTTP endpoint that exposes inventory is a documented projection for external clients (CI scripts, third-party monitors) — internal substrate consumers MUST use the bus. See P1.43.
  • A webapp. The gateway has no UI of its own. /healthz is JSON; /api/identity/* is JSON.
  • A federation layer. It serves one Host's apps. Cross-Host routing is out of scope for this package; federation has its own handshake spec.
  • An asset cache. Each request goes through to the source runtime. Caching is target state.

Legacy projection endpoints

Several endpoints exist today as compatibility projections of bus actors. They are kept for non-bus-aware external clients (CI scripts, uptime monitors); internal substrate consumers MUST migrate off them.

EndpointBus authorityStatus
GET /api/appssystem.catalogProjection only; internal consumers migrating (P1.43 step 3).
GET /api/statussystem.runtimesProjection only; internal consumers migrating.
GET /api/runtimes/*system.runtimesProjection only; control half (POST /api/runtimes/start) being moved to host.control invocations.
GET /api/configsystem.gateway.http.configProjection only.

Status: target state — projection or retirement, P1.43 step 7. Each projection endpoint is either retired entirely once internal consumers are migrated, or retained with an explicit "projection, not authority" comment block in the gateway source.

See also

Source: projects/matrix-3/packages/system-gateway-http/src/http-listener.ts, projects/matrix-3/packages/system-gateway-http/src/index.ts.