Appearance
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:
- Validates method (GET/HEAD only).
- Normalizes the path.
- Calls
_loadRoutesto get the current set of webapp routes. - Calls
_resolveRouteto pick a matching route. - If the route has an
assetMount(a Matrix asset endpoint), calls that mount via NATS RR with ophttp.asset.request. - If the route has an
origin(a fallback HTTP origin URL),fetch()to that origin. - 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:
- Host runtime records (
<host-home>/runtimes/<id>/runtime.json) — per-runtime files written by host-service whenmatrix upsucceeds. Read directly from the filesystem. - 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.spacePathis the canonical name (e.g.alt.stories.ghost-stories.funny);routeKeyis 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
- Routes / App routes
- Routes / Space routes
- Routes / Route resolution
- Architecture: HTTP gateway and asset resolution
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.