Skip to content

Component packaging

UI components ship as part of a package, alongside the headless actor that backs them. This page covers the matrix.json declarations that mark a package as having a browser surface, the dist/ layout the gateway serves, and the vertical-slice rule that says where UI code is allowed to live.

The vertical-slice rule, in one paragraph

Every UI component belongs to the package that owns the backend it talks to. Not the shell. Not matrix-web. The vertical package.

matrix-web and matrix-edge are pure shells — navbar, status bar, auth gate, surface discovery, layout. They contain ZERO domain UI code. If a component displays inference providers, it lives in the inference package; if it edits a user profile, it lives in the user package. See WORKSTREAMS/matrix-web/VERTICAL-SLICE-RULE.md for the full rule.

This page documents the mechanics of shipping a component-bearing package; the policy about who owns what UI is in that workstream doc.

matrix.json — declaring a webapp

A package that ships a UI component bundle declares a webapp block in its matrix.json. The simplest case (from projects/matrix-3/packages/director/matrix.json):

json
{
  "name": "@open-matrix/director",
  "version": "0.1.28",
  "description": "Director — Matrix actor hierarchy explorer",
  "runtime": { "language": "typescript", "entry": "dist/index.html" },
  "webapp": {
    "distDir": "dist",
    "entry": "index.html",
    "appName": "director",
    "displayName": "Director",
    "icon": "🧭",
    "navOrder": 20,
    "description": "Matrix actor hierarchy explorer",
    "shells": ["platform", "edge"]
  },
  "components": [],
  "permissions": { "fsPolicy": "none" }
}

The webapp block tells Host Service / system-gateway-http that this package serves static assets:

FieldWhat it does
distDirDirectory (relative to package root) that the gateway serves.
entryThe HTML entry inside distDir. The gateway serves it for the package's route.
appNameURL-safe slug; the package is reachable at /apps/<appName>/.
displayNameHuman-readable name shown in the shell's app launcher.
iconEmoji or short string shown in launcher.
navOrderSort order in the launcher (lower first).
descriptionTooltip / launcher description.
shellsWhich shells should list this app. "platform" = matrix-web (account shell), "edge" = matrix-edge (Device shell).

When you run matrix run @open-matrix/director --serve, Host Service:

  1. Allocates an HTTP port for the runtime.
  2. Registers the runtime with system.runtimes.
  3. Adds a route through system-gateway-http for /apps/director/ → that port.
  4. Serves the package's dist/ over that route.

Hybrid packages — components + actors

Hybrid packages declare both a webapp surface and a list of components (mounted actors). The chat package is the canonical example (projects/matrix-3/packages/chat/matrix.json):

json
{
  "name": "@open-matrix/chat",
  "class": "hybrid",
  "namespace": "chat",
  "root": {
    "type": "MatrixChatApp",
    "export": "MatrixChatApp",
    "mount": "chat",
    "surface": "browser"
  },
  "runtime": {
    "language": "typescript",
    "entry": "./dist/runtime/index.js",
    "browserEntry": "./dist/browser/register-elements.js",
    "bootstrapEntry": "./dist/browser/bootstrap.js",
    "environments": ["browser", "headless-dom", "runtime"],
    "executionClassCompatibility": [
      "shared_host_service", "dedicated_process",
      "container", "isolate", "remote_managed_service"
    ]
  },
  "webapp": {
    "distDir": "./dist/browser",
    "entry": "index.html",
    "appName": "chat",
    "shells": ["platform", "edge"]
  },
  "components": [
    { "type": "ChatConversationProxy", "mount": "conversation", "surface": "headless", "autoStart": true },
    { "type": "ChatComponentLibraryProxy", "mount": "component-library", "surface": "headless", "autoStart": true }
  ],
  "permissions": { "fsPolicy": "none", "network": true, "subprocess": false, "env": false }
}

The relevant fields for a component-bearing package:

  • runtime.browserEntry — the JS module that calls customElements.define('chat-*', ...) for every UI component the package ships. Browsers load this through the gateway-served HTML's <script type="module">.
  • runtime.bootstrapEntry — the bootstrap script the gateway-served HTML imports first. Sets up the MatrixDSLHost, calls createBrowserNatsTransport, mounts the root component.
  • components — headless actors auto-started by the runner when this package boots as a runtime. Each one becomes a mount under the package's namespace.
  • consumes (chat extends this) — declares cross-package contracts the package needs satisfied.

Build outputs

A typical component-bearing package builds to:

packages/chat/
├── matrix.json
├── package.json
├── src/
│   ├── runtime/                      ← headless actor implementations
│   ├── browser/                      ← UI components (custom elements)
│   │   ├── register-elements.ts
│   │   └── bootstrap.ts
│   └── ...
├── vite.config.ts                    ← gateway-served browser bundle
├── vite.runtime.config.ts            ← headless runtime bundle (Node)
└── dist/
    ├── runtime/index.js              ← runtime.entry
    └── browser/
        ├── index.html                ← webapp.entry
        ├── register-elements.js      ← runtime.browserEntry
        ├── bootstrap.js              ← runtime.bootstrapEntry
        └── assets/

The exact build setup varies. chat, director, flowpad, inference-settings, and smithers all use Vite. Some packages have a single vite.config.ts; others split into vite.config.ts (browser) + vite.runtime.config.ts (Node). When in doubt, use the closest existing package as a template.

Permissions

Every package declares a permissions block. UI-bearing packages typically need network: true (so the bootstrap can open a NATS WebSocket connection) and fsPolicy: "none" (no filesystem access from a browser). Headless backend actors inside the same package may need different permissions; the SDK does not enforce this at runtime — package supervisors and the browser sandbox do.

Shipping convention

A workshop-grade Matrix package satisfies these checks:

  • matrix.json declares a webapp block with appName, entry, shells.
  • matrix.json declares components for any headless actors that should auto-start.
  • The browser entry registers every UI component via customElements.define.
  • The bootstrap entry uses createBrowserNatsTransport({ wsUrl: '/nats-ws', ... }) (same-origin WS, never absolute).
  • Tests live alongside the package: unit tests in tests/, integration tests in projects/matrix-3/tests/.
  • The package builds idempotently from a fresh checkout: pnpm install && pnpm --filter <name> build.

Skeleton walkthrough — a brand-new component package

bash
# 1. Create the package directory and matrix.json
mkdir -p projects/matrix-3/packages/my-widgets/{src,dist}
cat > projects/matrix-3/packages/my-widgets/matrix.json <<'EOF'
{
  "name": "@open-matrix/my-widgets",
  "version": "0.1.0",
  "description": "Demonstration UI widgets",
  "runtime": { "language": "typescript", "entry": "dist/index.html" },
  "webapp": {
    "distDir": "dist",
    "entry": "index.html",
    "appName": "my-widgets",
    "displayName": "My Widgets",
    "icon": "🧩",
    "navOrder": 99,
    "shells": ["platform"]
  },
  "components": [],
  "permissions": { "fsPolicy": "none", "network": true }
}
EOF

# 2. Implement at least one component, register it, build via Vite,
#    install into the local Host:
pnpm --filter @open-matrix/my-widgets build
matrix run @open-matrix/my-widgets --serve --home /tmp/matrix-home

The full template lives in the existing minimal packages (director, flowpad, inference-settings).

See also

Source: projects/matrix-3/packages/director/matrix.json, projects/matrix-3/packages/chat/matrix.json, projects/matrix-3/packages/flowpad/matrix.json, projects/matrix-3/packages/inference-settings/matrix.json (real shipping examples).