Skip to content

HTTP gateway

The HTTP gateway is system.gateway.http, a singleton actor mounted on each Host. It is the production-grade adapter — it translates inbound HTTP requests into actor invocations and serves package webapp assets out of registered runtimes.

Source: projects/matrix-3/packages/system-gateway-http/src/index.ts lines 262–360 (SystemGatewayHttpActor).

The actor's surface

Verbatim from index.ts:262:

typescript
export class SystemGatewayHttpActor extends MatrixActor {
  static description = 'Matrix HTTP gateway actor for package webapp routes.';

  static override accepts: Record<string, unknown> = {
    'gateway.http.status': {
      description: 'List Host-visible webapp route records for this system runtime',
      options: 'object?',
    },
    'gateway.http.resolve': {
      description: 'Resolve an HTTP pathname to a Matrix asset endpoint or Host runtime origin',
      pathname: 'string',
      appName: 'string?',
    },
    'gateway.http.request': {
      description: 'Relay a GET or HEAD request through Matrix asset endpoints',
      pathname: 'string',
      appName: 'string?',
      method: 'string?',
      headers: 'object?',
    },
  };
}
OpWhat it does
gateway.http.statusList registered webapp routes on this Host
gateway.http.resolveMap a pathname to a route + origin (no fetch)
gateway.http.requestResolve + fetch in one call (GET/HEAD only)

How requests flow

HTTP client → host-http-runtime (Node http server)
            → reads pathname / method
            → invokes system.gateway.http
              op = 'gateway.http.request'
            → gateway.http resolves to a runtime origin
            → fetches asset from that runtime
            → returns body+headers to host-http-runtime
            → host-http-runtime writes the HTTP response

host-http-runtime.ts is the wrapper that owns the listener; it does not own routing. Routing lives in the system.gateway.http actor, so the gateway logic is testable through the bus and observable via $introspect like any other actor.

Route registration

Webapp packages declare their route in matrix.json:

json
{
  "name": "chat",
  "displayName": "Matrix Chat",
  "type": "webapp",
  "webapp": {
    "appName": "chat",
    "routePrefix": "/apps/chat",
    "displayName": "Matrix Chat",
    "icon": "...",
    "navOrder": 30,
    "shells": ["platform", "edge"]
  }
}

When the runtime starts, it registers itself with system.runtimes (runtimes.register) including the webapp metadata. The gateway reads runtime records to build the route table on demand. There is no separate "register-with-gateway" call; the runtime registration is the authority.

/api/apps projection

The gateway serves /api/apps as a read projection over its route table. From MATRIX-AUTHORITY-MODEL.md:

"/api/apps MUST be derivable from gateway routes that, in turn, derive from runtime metadata. No standalone hosted-apps store."

The shell calls /api/apps and renders the launcher; the runtime metadata is the single source.

Restricted methods

Only GET and HEAD are supported for asset serving (verbatim from index.ts:312):

typescript
async onGatewayHttpRequest(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
  const method = (readString(payload.method) || 'GET').toUpperCase();
  if (method !== 'GET' && method !== 'HEAD') {
    return {
      ok: false,
      statusCode: 405,
      headers: { allow: 'GET, HEAD' },
      error: 'gateway.http.request only supports GET and HEAD',
    };
  }
  // ...
}

POST/PUT/DELETE for actor ops do NOT flow through gateway.http.request. They go through HTTP routes the gateway explicitly mounts (/api/auth/..., /api/identity/..., etc.) which internally invoke specific actors. There is no automatic POST → arbitrary accepts mapping today.

Status: target state for full REST/OpenAPI bridge. A general POST /api/<mount>/<op> route that transparently maps to actor accepts ops is the REST/OpenAPI bridge design target. Today, only ops on the gateway's own surface (gateway.http.*) are exposed automatically.

Edge system-devices API

The gateway short-circuits a few appName=edge routes to expose Device-specific endpoints (onGatewayHttpRequest):

typescript
const edgeHostApiResponse = readString(payload.appName) === 'edge'
  ? await this._edgeHostApiResponse(normalized)
  : null;

This is how Matrix Edge surfaces Host status to the local edge UI.

SystemGatewayHttp parent

SystemGatewayHttpActor is mounted under a SystemGateway parent actor (also in index.ts). The parent supervises the HTTP child actor and reports gateway.status:

typescript
onGatewayStatus(): Record<string, unknown> {
  return {
    ok: this._httpMounted,
    mount: this.mount ?? 'system.gateway',
    children: [{
      id: 'http',
      mount: `${this.mount ?? 'system.gateway'}.http`,
      type: 'SystemGatewayHttpActor',
      status: this._httpMounted ? 'mounted' : 'failed',
      ...(this._httpError ? { error: this._httpError } : {}),
    }],
  };
}

The parent/child split lets the supervisor restart the HTTP layer without tearing down the whole gateway.

Authentication

The gateway terminates session authentication on inbound HTTP requests:

  • Reads mx_session cookie.
  • Calls auth.session.validate to derive the principal.
  • Attaches principal to the request context for downstream handlers.

Asset requests (/apps/<name>/...) go through this validation; cookie- less requests get the public asset surface only.

See also