Skip to content

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:

  • ChatActor mounts at chat (headless, claims chat.send / chat.thread.list).
  • MatrixHttpAssetEndpointActor mounts at chat.http (serves the dist).
  • The webapp record is advertised, so /apps/chat/... resolves.

The browser, on /apps/chat/:

  • Loads index.html from chat.http.
  • Bootstraps via /api/bootstrap.
  • Connects NATS WebSocket.
  • Mounts ChatBrowserComponent in the page (chat.ui).
  • The browser-side chat.ui calls the headless chat.send op via NATS RR.

Why both

Three reasons a package wants both:

  1. State persistence. The browser is ephemeral. Conversations, credentials, and cached state need a home. The headless actor owns it.
  2. 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.
  3. 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

ModeUse when
Asset endpoint (assetMount)Static built bundle, immutable assets
Origin-backedDev-mode hot reload, runtime-generated assets, server-side rendering
BothDev 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 $http routes 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.