Skip to content

URL to Matrix routing

This page is the end-to-end trace. Pick a URL: GET /apps/chat/assets/index.js. Watch every step from socket-accept to byte-emit.

Sequence

1. HTTP listener accepts the connection on port 3100.
2. _dispatch matches the path against grammar rules in order.
3. The /apps/<appName>/ rule wins; handler onWebapp is called.
4. onWebapp calls SystemGatewayHttpActor.gateway.http.request via NATS RR.
5. The actor calls _loadRoutes (joins host runtime records + system.runtimes
   data) to find the runtime serving appName="chat".
6. Resolution returns { routeKind, runtimeId, assetMount: 'chat.http' }.
7. The actor calls chat.http via NATS RR with op http.asset.request.
8. The asset endpoint actor (in the chat runtime's process) reads the file
   from dist/browser/assets/index.js, packs the bytes as base64, returns.
9. The gateway runtime receives the response, decodes the bytes, writes the
   HTTP response to the browser.

That is the entire flow. No special protocol, no Matrix-specific HTTP. The gateway is "an HTTP frontend that calls actors instead of files."

The URL grammar match

In _dispatch (http-listener.ts:117-208):

typescript
// Earlier rules win.
if (path === '/healthz') { ...; return; }
if (path === '/') { ...; return; }
if (path === '/api/bootstrap') { ...; return; }
if (path === '/api/apps') { ...; return; }
if (path === '/nats-ws') { ...; return; }
if (path === '/favicon.ico' || path.startsWith('/branding/')) { ...; return; }

// Multi-segment public routes try first
const publicNamespaceRoute = parsePublicNamespaceWebappRoute(path);
if (publicNamespaceRoute) { ... }
const spacePathRoute = parseRouteKeyWebappRoute(path);  // Space path; routeKey is the v1 alias
if (spacePathRoute) { ... }
const explicitAuthorityRoute = parseExplicitAuthorityWebappRoute(path);
if (explicitAuthorityRoute) { ... }

// Generic /apps/<appName>/...
if (path.startsWith('/apps/')) { onWebapp(...); return; }

// Control-plane fallthroughs
if (path === '/api/status') { ... }
// ... /api/host/stop, /api/runtimes/*, etc.

// Otherwise 404
onNotFound(...);

The order is significant. /apps/edge/... matches the generic webapp rule; /foo.bar/edge/... matches the public-namespace rule first because foo.bar parses as a typed public namespace.

The route resolution call

onWebapp calls into SystemGatewayHttpActor.onGatewayHttpRequest (system-gateway-http/src/index.ts:310-383). The actor:

  1. Validates method (GET/HEAD only).
  2. Normalizes the path.
  3. Calls _loadRoutes to get the current set of webapp routes.
  4. Calls _resolveRoute to pick a matching route.
  5. If the route has an assetMount (a Matrix asset endpoint), calls that mount via NATS RR with op http.asset.request.
  6. If the route has an origin (a fallback HTTP origin URL), fetch() to that origin.
  7. Returns the response (status, headers, body) wrapped in { ok, statusCode, headers, bodyEncoding, bodyBase64, resolution }.

Where routes come from

_loadRoutes joins two sources:

typescript
// system-gateway-http/src/index.ts:385-402
private async _loadRoutes(): Promise<IWebappRoute[]> {
  const routes = new Map<string, IWebappRoute>();
  for (const route of this._loadHostRuntimeRecordRoutes()) {
    routes.set(`${route.appName}�${route.runtimeId}�...`, route);
  }
  for (const route of await this._loadRuntimeRegistryRoutes()) {
    if (!routes.has(...)) { routes.set(...); }
  }
  return [...routes.values()].sort(...);
}

The two sources:

  1. Host runtime records (<host-home>/runtimes/<id>/runtime.json) — per-runtime files written by host-service when matrix up succeeds. Read directly from the filesystem.
  2. Runtime registry (system.runtimes.runtimes.registered) — a Matrix request/reply call to the runtime registry actor.

Records win over registry on key conflict. The registry exists for runtimes that don't write Host runtime records (browser-runtime tabs that mount webapp components don't have on-disk records).

Asset endpoint

For each webapp route the package's runtime mounts a MatrixHttpAssetEndpointActor at <appName>.http. That actor:

typescript
// system-gateway-http/src/index.ts:80-219
class MatrixHttpAssetEndpointActor extends MatrixActor {
  static accepts = {
    'http.asset.status': { ... },
    'http.asset.resolve': { pathname: 'string', appName: 'string?' },
    'http.asset.request': { pathname, appName?, method?, headers? },
  };
  // Reads from `assetEndpoint.distDir + assetPath` after path-traversal
  // validation. Returns base64 bytes inline up to MAX_RELAY_BODY_BYTES (10 MB).
}

The actor lives inside the package's own runtime — it has filesystem access to that package's dist/browser/. The gateway runtime does not.

Response shape

json
{
  "ok": true,
  "statusCode": 200,
  "statusText": "OK",
  "headers": {
    "content-type": "text/javascript; charset=utf-8",
    "content-length": "1234",
    "cache-control": "no-cache"
  },
  "bodyEncoding": "base64",
  "bodyBase64": "..."
}

The gateway then materializes the HTTP response: writeGatewayHeaders sets content-type and content-length; the bytes go straight to the ServerResponse.

Public-namespace and explicit-authority paths

The same flow applies for the multi-segment routes:

  • /<spacePath>/<appName>/<asset> — public Space route. spacePath is the canonical name (e.g. alt.stories.ghost-stories.funny); routeKey is a v1 compatibility alias for the same field.
  • /<publicNamespace>/<appName>/<asset> — typed public namespace (space.<spacePath>).
  • /<email>/<appName>/<asset> — explicit authority route.

These are parsed up-front (in http-routing.ts) into a route record that knows the canonical authority root. The gateway then issues a Matrix RR under that authority root's wire prefix instead of the local one.

That extra step lets HiveCast Edge serve "any user" — the routing knows to relay to a different Host's system.gateway.http instead of the local one.

See also

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