Appearance
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.jsonVocabulary note. "Space path" is the canonical name for what older code calls
routeKey. New docs and UI copy say Space path; the field namerouteKeysurvives 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:
| Endpoint | Purpose |
|---|---|
GET /api/identity/bootstrap | Returns 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-creds | Mints NATS user JWTs scoped to the authenticated principal. |
GET /oauth/<provider>/callback | OAuth providers redirect to HTTP URLs by protocol; this is unavoidable. |
GET /healthz | Liveness 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.
- "What apps are running?" →
- A webapp. The gateway has no UI of its own.
/healthzis 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.
| Endpoint | Bus authority | Status |
|---|---|---|
GET /api/apps | system.catalog | Projection only; internal consumers migrating (P1.43 step 3). |
GET /api/status | system.runtimes | Projection only; internal consumers migrating. |
GET /api/runtimes/* | system.runtimes | Projection only; control half (POST /api/runtimes/start) being moved to host.control invocations. |
GET /api/config | system.gateway.http.config | Projection 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
- URL to Matrix routing — the resolve flow
- App hosting / Static app packages
- NATS WebSocket
- Routes / API compatibility routes
Source:
projects/matrix-3/packages/system-gateway-http/src/http-listener.ts,projects/matrix-3/packages/system-gateway-http/src/index.ts.