Skip to content

Runtime entrypoints

A package can have up to three different entry files, depending on what it ships. This page covers what each one is, who loads it, and what the contract is for what it must export.

The three entries

json
"runtime": {
  "language": "typescript",
  "entry": "./dist/runtime/index.js",
  "browserEntry": "./dist/browser/register-elements.js",
  "bootstrapEntry": "./dist/browser/bootstrap.js"
}
EntryLoaded byRequired when
entryThe runner (Host Service) when the package is brought up as a runtime.Package has headless components OR is a webapp that needs entry: "dist/index.html" sentinel.
browserEntryThe browser, via the gateway-served HTML's <script type="module">. Registers custom elements.Package ships UI components.
bootstrapEntryThe browser, via the same HTML, BEFORE browserEntry. Sets up transport, host element, root mount.Package ships UI components.

A purely-headless package (e.g., system-auth) declares only entry. A purely-UI package (e.g., director) declares entry: "dist/index.html" and no separate browser/bootstrap entries because its bootstrap is bundled into the same HTML by Vite. A hybrid (e.g., chat) declares all three.

runtime.entry — what the runner imports

The runner calls await import(packagePath + entry), then iterates manifest.components[] and looks up module[component.export] for each entry. Each export must be a MatrixActor subclass (or a class registered as a custom element).

ts
// dist/runtime/index.ts
export { ChatConversationProxy } from './chat-conversation-proxy';
export { ChatComponentLibraryProxy } from './chat-component-library-proxy';
export { ChatIdentityProxy } from './chat-identity-proxy';
// ... etc, one per components[] entry

If components[] is empty (e.g., a webapp-only package), the runner simply imports the module for any side effects and continues. The sentinel "entry": "dist/index.html" (used by director, flowpad, inference-settings) is the convention for "no JS entry" — the runner sees no JS to import, only the gateway serves the HTML.

Note: The runner does not call any factory function exported by entry. The protocol is "import, then look up the named exports listed in components[]." If you need package-wide initialization, do it in module top-level code (it runs once per import) — but prefer per-actor onBootstrap().

runtime.bootstrapEntry — browser bootstrap

Loaded first by the served HTML. The bootstrap is responsible for:

  1. Constructing a transport using createBrowserNatsTransport({ wsUrl: '/nats-ws', ... }).
  2. Resolving the authority root (typically from a /api/whoami endpoint or a meta tag).
  3. Constructing a MatrixRuntime({ transport, domStrategy: new BrowserDomStrategy() }).
  4. Inserting a <matrix-dsl-host> element into the DOM and giving it the runtime + the root context.
  5. Mounting the root component (manifest.root.export) under the host.

Sketch (every package writes its own variant, but the shape is similar):

ts
// src/browser/bootstrap.ts
import { MatrixRuntime, BrowserDomStrategy, createBrowserNatsTransport } from '@open-matrix/core';
import { ChatApp } from './chat-app';

async function main() {
  const whoami = await fetch('/api/whoami').then(r => r.json());

  const transport = await createBrowserNatsTransport({
    wsUrl: '/nats-ws',
    root: whoami.authorityRoot,
    jwtProvider: () => fetch('/api/nats-jwt').then(r => r.json()),
  });

  const runtime = new MatrixRuntime({
    transport,
    domStrategy: new BrowserDomStrategy(),
  });

  customElements.define('chat-app', ChatApp);
  // ... other UI components

  const host = document.querySelector('matrix-dsl-host')!;
  // wire host with runtime; mount <chat-app> as root
}

main();

This is illustrative — package bootstraps in the tree (chat, director, flowpad) are the canonical references. The exact API of <matrix-dsl-host> and the bootstrap sequence is in @open-matrix/core/browser/MatrixDSLHost.

runtime.browserEntry — element registration

browserEntry is the module that calls customElements.define for every UI component the package ships:

ts
// src/browser/register-elements.ts
import { ChatConversationView } from './components/chat-conversation-view';
import { ChatInput } from './components/chat-input';
import { ChatMessageList } from './components/chat-message-list';

customElements.define('chat-conversation-view', ChatConversationView);
customElements.define('chat-input', ChatInput);
customElements.define('chat-message-list', ChatMessageList);

In simple packages, registration happens inline in bootstrapEntry, and browserEntry is omitted. In larger packages, splitting them keeps bootstrap focused on transport/runtime setup and keeps registration where the components live.

The HTML loads them in order:

html
<script type="module" src="./bootstrap.js"></script>
<script type="module" src="./register-elements.js"></script>

Module dependency order matters: bootstrap first because it sets up the runtime and host element; then registrations so elements can find their host on construction.

Vite configuration

Most packages use Vite for the browser bundle. A typical vite.config.ts produces:

dist/browser/
├── index.html
├── bootstrap.js
├── register-elements.js
└── assets/<hashed>.js

For Node-side runtime.entry, packages typically use plain tsc (compiling src/runtime/ to dist/runtime/). Some packages use a vite.runtime.config.ts that produces an ESM bundle for the runtime side too — useful when the runtime depends on third-party CJS that needs to be bundled.

The exact build script lives in each package's package.json. There is no single canonical layout — pick the closest existing package as a template (e.g., copy from flowpad or director for webapp-only, from chat for hybrid).

What the runner is allowed to assume

  • The entry module loads cleanly under Node ESM with no top-level await blocking startup more than a few seconds.
  • Every export named in components[] is a class (constructor function), not a factory.
  • The class is a valid MatrixActor subclass (passes instanceof checks at runtime).
  • The class has whatever static fields its declared surface, accepts, emits imply.

If any of these break, matrix run reports the specific failure and the supervisor refuses to start the runtime.

See also

Source: projects/matrix-3/packages/chat/matrix.json:14-26 (all three entries present), projects/matrix-3/packages/director/matrix.json:5 (entry: "dist/index.html" sentinel pattern).