Skip to content

App surfaces

A package becomes a web app by adding a webapp block to matrix.json. The block declares the app's slug, the build output directory, the entry HTML, and the shells that should expose it.

The minimum viable webapp block

json
{
  "webapp": {
    "distDir": "dist",
    "entry": "index.html",
    "appName": "my-app",
    "displayName": "My App"
  }
}

That alone is enough for mx run . --serve to start a server and for the Host gateway to route /apps/my-app/.

All recognized fields

From packages/mx-cli/src/utils/webapp-manifest.ts:

FieldRequiredPurpose
distDiryesDirectory containing built browser assets, relative to package root
entryyesEntry HTML file inside distDir (typically index.html)
appNameyesURL slug; gateway routes /apps/<appName>/ to this app
displayNamerecommendedHuman-readable title shown in shells and nav
descriptionoptionalOne-line description
iconoptionalEmoji/glyph used in nav
navOrderoptionalSort order in shell navigation
shellsoptionalArray of shell ids that should expose this app (platform, edge)
baseoptionalBase path mode; "relative" is the supported value today

How it gets served

When you run mx run . --serve or matrix up <pkg> --serve, the runner mounts a MatrixHttpAssetEndpointActor for the package (packages/mx-cli/src/commands/run.ts:526-543):

ts
const assetMount = buildMatrixHttpAssetMount(webapp.appName, runtimeId);
await runtime.createSupervised(MatrixHttpAssetEndpointActor, assetMount, {
  assetEndpoint: {
    packageName: webapp.packageName,
    appName: webapp.appName,
    distDir: webapp.distDir,
    entryFile: webapp.entryFile,
  },
});

The runner registers an IRunnerWebappRouteMetadata record with the runner control plane. The Host gateway picks it up and starts routing /apps/<appName>/* to the runtime's asset endpoint.

For --serve, an additional same-origin HTTP server (from packages/mx-cli/src/utils/standalone-webapp-server.ts) starts on a free port and exposes:

  • /apps/<appName>/ — the static assets
  • /bootstrap (or as configured) — the bootstrap script
  • /nats-ws — same-origin NATS WebSocket relay

CLAUDE.md Rule 7 requires the browser to connect via the same-origin WebSocket. The standalone server enforces this.

Required directory layout

my-app/
├── matrix.json                 ← webapp block declares distDir: "dist"
├── package.json
├── src/
│   └── ...
└── dist/                       ← output of pnpm build
    ├── index.html              ← matches webapp.entry
    └── assets/
        └── ...

dist/ is build output. Don't commit it; the build script regenerates it.

Live examples

director — pure web app

json
{
  "name": "@open-matrix/director",
  "runtime": { "language": "typescript", "entry": "dist/index.html" },
  "webapp": {
    "distDir": "dist", "entry": "index.html",
    "appName": "director", "displayName": "Director",
    "icon": "🧭", "navOrder": 20, "shells": ["platform", "edge"]
  },
  "components": []
}

Director has no service factory. It is pure browser code; the runtime just serves its assets.

chat — hybrid

json
{
  "name": "@open-matrix/chat",
  "class": "hybrid",
  "runtime": {
    "entry": "./dist/runtime/index.js",
    "browserEntry": "./dist/browser/register-elements.js"
  },
  "webapp": { "distDir": "./dist/browser", "entry": "index.html",
    "appName": "chat", "shells": ["platform", "edge"], "base": "relative" },
  "components": [/* nine actor proxies */]
}

Chat ships both a service factory (whose actors run inside the host process) and a webapp (whose custom elements run in the browser tab). The two halves communicate over the same NATS bus.

matrix-web — webapp + HTTP routes

json
{
  "name": "@open-matrix/matrix-web",
  "webapp": { "distDir": "dist", "entry": "index.html",
    "appName": "web", "displayName": "HiveCast Portal",
    "navOrder": 0, "shells": ["platform"] },
  "http": {
    "routes": [{
      "export": "MatrixWebHttpSurface",
      "prefixes": ["/api/identity/", "/.well-known/", "/auth/", ...]
    }]
  },
  "components": [/* identity, attach-broker, billing */]
}

matrix-web adds an http block whose routes attach to the Host gateway in addition to the standard /apps/web/ asset route.

Vertical slice rule

CLAUDE.md Rule 2 says every package with a user-facing function ships both a backend actor AND a browser UI component. The shell (matrix-web) contains zero domain UI — only navbar, status bar, auth gate, surface discovery, layout. A new app belongs to its own package, never inside the shell.

See WORKSTREAMS/matrix-web/VERTICAL-SLICE-RULE.md for the canonical statement.

See also

Source: projects/matrix-3/packages/mx-cli/src/utils/webapp-manifest.ts reads/validates the block; packages/mx-cli/src/commands/run.ts:526-559 mounts the asset endpoint and routes the gateway entry.