Appearance
Runtime-backed apps
Some Matrix packages have both a browser surface and a headless service. Chat is the canonical example: the browser shows the conversation UI; a headless ChatActor in the package's runtime processes messages, integrates with inference, and persists state.
Package shape
json
{
"name": "@open-matrix/chat",
"webapp": {
"appName": "chat",
"displayName": "Chat",
"shells": ["platform"]
},
"components": [
{
"type": "ChatActor",
"mount": "chat",
"surface": "headless",
"autoStart": true,
"accepts": ["chat.send", "chat.thread.list"],
"emits": ["chat.message"]
},
{
"type": "ChatBrowserComponent",
"mount": "chat.ui",
"surface": "browser",
"autoStart": false
}
]
}When the runtime starts:
ChatActormounts atchat(headless, claimschat.send/chat.thread.list).MatrixHttpAssetEndpointActormounts atchat.http(serves the dist).- The webapp record is advertised, so
/apps/chat/...resolves.
The browser, on /apps/chat/:
- Loads
index.htmlfromchat.http. - Bootstraps via
/api/bootstrap. - Connects NATS WebSocket.
- Mounts
ChatBrowserComponentin the page (chat.ui). - The browser-side
chat.uicalls the headlesschat.sendop via NATS RR.
Why both
Three reasons a package wants both:
- State persistence. The browser is ephemeral. Conversations, credentials, and cached state need a home. The headless actor owns it.
- Authority. Some operations require credentials that must not leave the local machine (per CLAUDE.md Rule 4 — Factotum). The headless actor performs them; the browser delegates.
- Multi-client. Multiple browser tabs / Devices can see the same conversation because the headless actor is the source of truth.
Origin-backed alternative
An "origin-backed" route is a different model: the runtime exposes an HTTP origin (e.g. a Vite dev server, an Express server) and the gateway fetch()-es from it instead of going through MatrixHttpAssetEndpointActor.
typescript
// system-gateway-http/src/index.ts:545-589
private async _fetchOriginBackedRoute(resolution, request) {
// ... fetch(targetUrl, ...) → relay response
}This is used for:
- Local development with a hot-reloading dev server (Vite).
- Apps that need full HTTP-shaped behavior (custom routes, POST handlers, streaming responses) that the asset endpoint doesn't provide.
The webapp record carries an origin field instead of (or in addition to) assetMount. Origin always wins when both are present (_resolveRoute selection priority).
When to use which
| Mode | Use when |
|---|---|
Asset endpoint (assetMount) | Static built bundle, immutable assets |
| Origin-backed | Dev-mode hot reload, runtime-generated assets, server-side rendering |
| Both | Dev mode (origin) and production (asset). Same package, different runtime config. |
Custom HTTP per package
The asset endpoint serves only files. Packages that need custom HTTP behavior (POST handlers, streaming, server-side rendering) currently use the origin-backed route, running their own HTTP server inside the runtime.
Status: Native Matrix-actor-served HTTP (declared
$httproutes per the architecture doc) is target state. Today the choices are: serve static files (asset endpoint) or run a real HTTP server (origin-backed).
Cross-package coordination
Runtime-backed apps coordinate with each other through ordinary Matrix request/reply. ChatActor calls system.inference.openai.inference.complete to get LLM responses. DirectorActor calls chat.thread.list to introspect. None of this involves the gateway; the gateway is only for browser-facing HTTP.
See also
Source:
projects/matrix-3/packages/system-gateway-http/src/index.ts:514-589.