Appearance
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:
| Field | Required | Purpose |
|---|---|---|
distDir | yes | Directory containing built browser assets, relative to package root |
entry | yes | Entry HTML file inside distDir (typically index.html) |
appName | yes | URL slug; gateway routes /apps/<appName>/ to this app |
displayName | recommended | Human-readable title shown in shells and nav |
description | optional | One-line description |
icon | optional | Emoji/glyph used in nav |
navOrder | optional | Sort order in shell navigation |
shells | optional | Array of shell ids that should expose this app (platform, edge) |
base | optional | Base 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
- Overview → Package vs app
- Authoring → Package manifest
- Authoring → Runtime entrypoints
- Local Development → Run in place
- Deployment Profiles → Gateway routes
Source:
projects/matrix-3/packages/mx-cli/src/utils/webapp-manifest.tsreads/validates the block;packages/mx-cli/src/commands/run.ts:526-559mounts the asset endpoint and routes the gateway entry.