Skip to content

Package vs actor

A package and an actor live at different layers of Matrix and have different lifecycles. Package authors who collapse them invariably get stuck on naming, mounts, and registration.

The two definitions, side by side

AspectPackageActor
LayerDistributionRuntime bus
Lives inA directory + tarballA process / browser tab
Identitynpm name (@open-matrix/chat)Mount path (chat.conversation)
Stored inRegistry, package store on diskThe bus address space
Lifecycleinstall → uninstallmount → unmount
PluralA directory contains 0..n actor classesA package may mount 0..n actor instances
Versioned bysemver (package.json:version)implicit in package version + actor contract

A package is what you publish. An actor is what runs.

Where the bridge lives in matrix.json

The bridge from package to actor is the components array. Each entry declares one actor class the package exports and where it mounts:

json
{
  "type": "ChatConversationProxy",
  "export": "ChatConversationProxy",
  "mount": "conversation",
  "surface": "headless",
  "autoStart": true
}

Validator rules from manifestValidator.ts:158-244:

  • components MUST be an array (may be empty)
  • type is required, non-empty string
  • export is optional but must be a non-empty string when provided
  • mount is optional but must be a non-empty string when provided
  • autoStart is boolean
  • props is an object
  • fsPolicy (per-component override) is one of none | matrix-only | project-readonly | project-readwrite | global-readonly

The mount in matrix.json is relative. The full bus address comes from prepending the package mount root (or the runtime root) at mount time — see Authoring → Actor declarations.

A concrete example: chat

projects/matrix-3/packages/chat/matrix.json is one package, but it declares nine actor components, each mounted under a different relative name (security-realm, topic-claims, conversation, component-library, identity, preferences, session-state, mcp, export). The package is @open-matrix/chat. The actors are nine different bus participants. They all ship together because they collaborate.

A concrete counter-example: a one-actor package

A package may declare zero actor components if it is a web app whose client-side custom elements are the actors:

json
{
  "name": "@open-matrix/director",
  "components": [],
  "webapp": { ... }
}

The browser tab mounts the actors itself when the user loads /apps/director/. The headless runtime here has nothing to do.

Practical implications for authors

  1. Name your package once, name your actors locally. The package name is global; component mounts are relative inside the package namespace.
  2. A new actor does not justify a new package. Add a components[] entry instead. Making a new package is appropriate when the actors serve a different audience, ship on a different cadence, or have a different dependency footprint.
  3. The npm version is the package version. Actors are versioned only through the package and through additive contract evolution (CLAUDE.md Rule 9: never remove fields, only add optional ones).

See also

Source: projects/matrix-3/packages/mx-cli/src/utils/manifestValidator.ts validates the components[] shape; sample manifests in packages/system/matrix.json and packages/chat/matrix.json.