A guide · Chord v2 · 2026-10-31

Hosts
and Plugins

Build a small notes app step by step, from one host to plugins on a server and on workers, edited while it runs

3kinds of host in an app: server, worker, client
1file per host kind in a plugin
0restarts to apply a plugin edit
1rule: plugins talk only through object types and registries

A tutorial for people building apps on Chord v2, written by the redesign session from design.md, slice-7c.md, slice-10.md, the decision log and the code in packages/chord/src/next and src/next-node. API names are the current ones; where slice 10's fix round or the app kit (chord/next/app, being designed) will change what app code writes, a note says so.

Before we begin

You want to build a notes app. You type a note in a browser tab, and every other tab sees it at once. Each project gets its own notes in its own process, so a project that misbehaves cannot take the others down. And you, or an agent you work with, add features while the app runs: a search, an export button, a word count, without restarting anything.

Chord gives you three things for that. Hosts: the processes of your app (the server, a worker per project, the browser tabs), connected to each other. Objects: the state and methods your app is made of, living on one host and used from any other. Plugins: directories of code that declare objects for each kind of host, loaded and reloaded while the app runs.

This book builds the app in that order. Each chapter adds one concept and shows complete code.

Seven things to know

  1. A host is one participant. The server, each worker, each browser tab and each terminal UI is a host; hosts connect over links. Tabs and terminal UIs are clients: no address, they connect to the server and ask.
  2. An object has a type, and a type is a contract. The contract names the object's state and methods; any host can ask for an object by type and params, and Chord finds where it lives.
  3. A facet declares, an object acts. A facet is a plugin's code for one host kind: it says which object types this host serves. The objects hold the state and do the work.
  4. A plugin is a directory. A package.json with the plugin's name, and one file per host kind: server.ts, worker.ts, web.tsx.
  5. The server builds, every host evaluates. The plugins component builds each facet into a small script; the server, workers and tabs load the scripts for their kind.
  6. Plugins meet through types and registries. One plugin uses another's objects through its contract, or contributes to a registry another defines. Nothing else crosses between plugins.
  7. An edit is a rollout. Change a file, build, roll out: the server, then every worker, swap the plugin's objects in place; a broken edit keeps the old code running and reports why.
ON DISKSERVER PROCESSCLIENTS AND WORKERSplugins/notes/server.ts`, `worker.ts`,`web.tsx`, `tui.ts`, `contract.tssrc/ (base code)`@app/core`: host kinds, principalsplugins componentdiscover, build every facet, watch,roll outserver hostserver facets; routes Project →workersclient: tab or TUIweb or tui facets, local UI objects;holds `Notes { p1 }`worker p1worker facets; serves `Notes { p1 }`read, buildhost packagesserver scriptsweb or tui scriptshold Notes { p1 }start (worker scripts), relayEach host loads only its own kind's facets. Plugins meet only through object types and registries.
Figure 1. How it all fits: plugins on disk are built once on the server; each host evaluates the facets of its kind; tabs and the TUI are clients that reach workers' objects through the server.

Words used throughout

A host is one participant of the app, made with createHost. A host kind (Server, Session) describes addressable hosts; an address (Session({ id: "p1" })) names one. An object type is a contract: local (used on its own host only) or remote (used from any host). An object is one instance of a type, per params. A facet is a plugin's declarations for one host kind. A plugin is a directory with a package.json. A reference is what you get when you ask for an object (host.get, self.get, ctx.get). A document is data an object stores, changed in transactions; a table holds many records with queries, in the host's store; a derive is a value computed from others; a native is an outside resource an object opens and closes. A registry is an extension point: one plugin defines it, others contribute. A rollout applies a build to the hosts in order.

I

One Host

The smallest app: one process, one object type, one call.

Start without plugins. One host, one object that keeps notes:

import { createHost, defineFacet, defineState, local, method, obj, arr, str } from "@earendil-works/chord/next";

const NoteList = obj({ items: arr(str()) });          // one schema, used by the contract and the document

// the contract: what a Notes object offers
const Notes = local("notes.store", {
	version: 1,
	params: {},                                        // one object per host: no params
	members: {
		list: NoteList,                               // state anyone can read and watch
		add: method(obj({ text: str() })),            // a method: arguments checked by Chord
	},
});

// the document the object stores (its own version: storage can change without changing the contract)
const NotesDoc = defineState("notes", { schema: NoteList, version: 1, initial: () => ({ items: [] }) });

// the facet: which types this host serves, and how to make each object
const facet = defineFacet((host) => {
	host.serve(Notes, (_params, self) => {
		const doc = self.state(NotesDoc);
		return {
			list: doc,
			add: async ({ text }, ctx) => self.tx(async (tx) => { (await tx.draft(doc)).items.push(text); }, ctx),
		};
	});
});

const server = createHost({ onError: console.error });
await server.load([{ name: "@app/notes", facet }], ctx);
const notes = await server.get(Notes, {}, ctx);        // a reference: live, with its state
await notes.add({ text: "buy milk" }, ctx);
console.log(notes.list.value);                          // { items: ["buy milk"] }

Four things happened. The contract Notes says what the object offers; the document says what it stores. Here they have the same shape, so one schema serves both; they are separate because they change separately (an object may offer a computed value or a shortened view instead of what it stores, and storage can change its shape without users noticing). The facet says this host serves Notes, with a factory that declares the object's parts: a document, and a method writing it in a transaction. load publishes the facet. get returns a reference once the object is live; its members are the contract's.

The factory only declares: it is synchronous and opens nothing. Chord activates the object (loads its documents, opens what it asked to open) and then serves calls. That is why the same factory can run again when the code changes.

II

An Object's Parts

Documents, tables, transactions, computed values, watching, and outside resources: the vocabulary of every factory.

A factory builds an object from a handful of parts, each declared with one call on self. The notes object grows to show all of them.

Documents hold the object's data: self.state(NotesDoc) gives a handle whose .value is the current data. Data changes only in a transaction: self.tx(fn, ctx) runs fn with tx, and await tx.draft(handle) gives a mutable copy of the document's data; whatever you change on it is committed at once when fn returns, or not at all if it throws.

add: async ({ text }, ctx) => self.tx(async (tx) => {
	const draft = await tx.draft(doc);          // the document's data, writable inside this transaction only
	draft.items.push(text);                      // recorded as a change, committed when the function returns
}, ctx),

Documents live in memory unless they are persisted: defineState(..., { persist: true, checkpointWhen }) keeps one in the host's store (a SQLite file the app opens and gives to createHost({ store })), loaded when the object activates. For many records with queries, an object declares a table:

const NoteRows = defineTable("notes", {
	version: 1,
	columns: { id: str(), text: str(), at: int() },
	key: ["id"],
	indexes: { by_time: ["at"] },
});

host.serve(NoteStore, (_params, self) => {               // a local type with empty params: it may persist
	const rows = self.table(NoteRows);                    // created (or migrated) when the object activates
	const recent = query(NoteRows).orderBy((n) => n.at, "desc");
	return {
		add: async ({ text }, ctx) => self.tx(async (tx) => tx.put(rows, { id: uuid(), text, at: Date.now() }), ctx),
		page: async ({ after }, ctx) => rows.page(recent, 50, ctx, { after }),   // 50 at a time, with a cursor
	};
});

Persisted data lives only in local objects with empty params (one per host and type), so its address never depends on who asked or with which params; an object with params (a view per project) reads from such a store. Writes go through the same transactions as documents: a note row and a document can change in one commit.

A derive is a value computed from other values, recomputed whenever they change. It is how an object offers something it does not store:

const count = self.derive([doc], ([notes]) => notes.items.length);   // 0, 1, 2, … as notes are added
return { list: doc, count, add };                                      // `count` can be a member like `list`

Watching is how anyone follows a value as it changes: a document handle, a derive, a reference's member. watch gives the current value, then one call per change:

const watch = await notes.count.watch(ctx);
watch.start(async (call) => {
	if (call.kind === "value") console.log("notes:", call.value);   // "failed" calls say a value cannot be computed now
});

A routine is the object watching on its own behalf: self.on(source, fn) runs fn with each new value, for as long as the object lives (a reconciler: "whenever the notes change, rebuild the export file").

A native is something outside Chord the object opens and must close: a file watcher, a database connection, a child process. self.open(open, close) opens it while the object activates and closes it when the object ends:

const inbox = self.open(async (ctx) => watchDirectory(inboxPath, ctx.abortSignal), (watcher) => watcher.close());
self.onLive(async (ctx) => {                                  // a routine started once the object is live
	for await (const file of inbox.value.files(ctx.abortSignal)) await importNote(file, ctx);
});

Natives are why some objects restart when their plugin changes: an open connection cannot be moved to new code, so Chord closes it and opens it again. Objects without natives are swapped in place.

III

Several Hosts

A server, a worker per project, browser tabs: how they find each other's objects.

Now give each project its own process. Declare the kinds of addressable host, and make notes a remote type that lives on its project's worker:

export const Server = hostKind("server");
export const Project = hostKind("project", { id: str() });

export const Notes = remote("notes", {
	version: 1,
	params: { project: str() },
	hostAddress: ({ project }) => Project({ id: project }),   // where an object of these params lives
	variants: { editor: { state: { list: NoteList }, methods: { add: method(obj({ text: str() })) } } },
});

Two things are new. A remote type can be used from any host, and it has variants: different sets of members for different callers, chosen by the factory (an editor variant with add, a reader variant for guests without it). And hostAddress says where its objects live; it is the whole routing story.

A tab asks for Notes { project: "p1" }. Its host computes the address, Project(p1), and sends the request to its parent, the server. The server has a route for Project: it starts (or finds) the worker for p1 and passes the request on. The worker serves Notes. The tab gets a reference whose state follows the worker's document, one frame per change.

CLIENTSSERVERWORKERStab Aholds Notes { project: p1 }tab Bholds Notes { project: p1 }serverroute Project → workerworker p1serves Notes { p1 }worker p2serves Notes { p2 }holdholdrelay
Figure 2. A tab holds a project's notes: the server routes by address to the project's worker and relays, without reading the data.

Each host is created the same way, with what it is:

// server.ts
const server = createHost<P>({ address: Server(), principal: { kind: "server" }, onError });

// worker.ts (started by the server for one project)
const worker = createHost<P>({ address: Project({ id }), principal: { kind: "worker", project: id }, onError });
worker.connect(parentLink, { principal: { kind: "server" }, trusted: true }, ctx);

// a browser tab: no address, it is a client
const tab = createHost<P>({ onError });
tab.connect(webSocketLink(new WebSocket("/chord")), { principal: { kind: "server" } }, ctx);

P is your app's type of principal, who is calling: an owner, a guest, the server, a worker. Links stamp it; a remote object is made per principal, so a guest can get a read-only variant of the same notes.

Coming with the app kit

Starting workers, routing a host kind to them, the WebSocket entry and authentication are slice 10's components today (createWorkers, chord.http), wired by the app. The opinionated app kit (chord/next/app, being designed) will do this wiring for you: startApp(...) and startWorker().

IV

Client Hosts

Browser tabs and terminal UIs are hosts too: what they can do, and what they cannot.

A browser tab and a terminal UI are hosts like the server, with one difference: they have no address. Nobody can send a request to a tab; a tab only connects to the server and asks for things. That is a deliberate rule: a tab runs on a device the server does not control, so nothing on the server ever waits for, or trusts, a tab's state.

// a browser tab (slice 11's page host will do this wiring for you)
const tab = createHost<P>({ onError });                               // no address, no principal: a client
tab.connect(webSocketLink(new WebSocket("/chord")), { principal: { kind: "server" } }, ctx);
await tab.load([{ name: "@app/web", facet: webCore }, ...webFacets], ctx);

const notes = tab.hold(Notes, { project: "p1" }, ctx);               // a reference at once, live soon
(await notes.list.watch(ctx)).start(async (call) => {
	if (call.kind === "value") render(call.value.items);              // every change, as it commits on the worker
});
await notes.add({ text: "from the tab" }, ctx);                        // a call: tab → server → worker

Who the tab is is not the tab's to say. The server authenticates the WebSocket when it connects (a cookie, the owner on loopback, a guest's share link) and stamps that principal on everything the tab asks for. The worker's Notes factory receives it and may give a guest a read-only variant. A tab cannot claim to be the owner.

What a tab serves is only local types: state of this tab's UI, never shared. A draft that survives switching panels, a selection, a tab-local cache:

// plugins/notes/web.tsx: the web facet serves a local type, and contributes a panel
export default defineFacet((host, self) => {
	host.serve(NoteDraft, (_params, self) => ({ text: self.state(DraftDoc) }));   // lives in this tab only
	self.contribute(Panels, { title: "Notes", Component: NotesPanel });
});

When the connection drops (a phone in a tunnel, a server restart), the tab keeps its references. Their values stay readable, their state says loading, calls made meanwhile wait; on reconnect every reference is held again and gets the current value in one frame. The UI shows a banner from ref.state, nothing more.

A terminal UI is the same in Node: a client host connected to the server over a WebSocket, loading the plugins' tui facets, rendering with whatever terminal library the app chose. What a client never does: serve a remote type, be routed to, or keep data others rely on. Data that must outlive a tab belongs in an object on the server or a worker.

V

Plugins

Move the code into a directory that can be added, edited and removed while the app runs.

So far the facets were part of the app's own code. A plugin is the same code in its own directory:

plugins/notes/
  package.json        { "name": "@app/notes" }
  contract.ts         the Notes contract (other plugins import it)
  worker.ts           the facet for workers: serves Notes
  server.ts           the facet for the server: an export route
  web.tsx             the facet for browser tabs: a notes panel

The plugin's name is its package.json name. Each file named after a host kind is that kind's facet, its default export a defineFacet(...). A plugin has only the files it needs: a plugin that only adds a route has a server.ts and nothing else.

// plugins/notes/worker.ts
import { defineFacet } from "@app/core";
import { Notes } from "./contract.ts";

export default defineFacet((host) => {
	host.serve(Notes, ({ project }, principal, self) => {
		const doc = self.state(NotesDoc);
		return { variant: "editor", members: { list: doc, add: async ({ text }, ctx) => /* … */ } };
	});
});

@app/core is your app's base code, imported by every plugin as a host package (the plugin's build leaves it out; every host passes its own copy in). It holds what plugins share and never reloads: host kinds, the principal type, a defineFacet bound to it.

VI

Loading Plugins

Two steps in two places: building a plugin into a script happens once, on the server; evaluating it happens in every process that runs it.

A plugin is source code on the server's disk: TypeScript files that import each other, import other plugins' contracts, import npm packages, and import things the app itself provides. None of the processes that run the plugin can use those files as they are. A browser tab has no file system and no TypeScript; a worker could read the files, but then every worker would compile every plugin again, and two workers started a minute apart could see two different versions. So loading a plugin is two separate steps:

  1. Build: turn each plugin facet (notes/worker.ts, notes/web.tsx) into one self-contained script. This happens once per edit, on the server, for every host kind.
  2. Evaluate: run a script inside one process, which gives back the facet, ready for host.load. This happens in every process that hosts that kind: the server for its own facets, each worker for the worker facets, each tab for the web facets.

The two steps need different things, and that is why the API has two calls.

Host packages: what a script leaves out

Look at what notes/worker.ts imports:

import { defineFacet, Session } from "@app/core";             // the app's base code
import { obj, str, remote } from "@earendil-works/chord/next";  // Chord itself
import { Notes } from "./contract.ts";                            // the plugin's own file
import slugify from "slugify";                                    // an npm package in the plugin's dependencies

The last two go into the script: the plugin's own files and its npm dependencies are copied in. The first two must not. If the script carried its own copy of Chord, the worker would run two Chords: the plugin's ChordError would not be the worker's ChordError, and the plugin's defineFacet would make a facet the worker's host does not recognize. The same holds for @app/core: the plugin must see the very Session host kind and the very Panels registry the process already has, not a copy.

Packages the process already has, and that every plugin must share, are its host packages. A script leaves them out and asks for them by name when it runs. Building therefore needs only their names: "do not copy @earendil-works/chord/next or @app/core into this script".

Building: createPlugins, on the server

const plugins = createPlugins({
	roots: ["./plugins"],                                 // where the plugin directories are
	kinds: {
		server: { platform: "node", hostPackages: ["@earendil-works/chord/next", "@app/core"] },
		worker: { platform: "node", hostPackages: ["@earendil-works/chord/next", "@app/core"] },
		web: { platform: "browser", hostPackages: ["@earendil-works/chord/next", "@app/core", "react", "react/jsx-runtime"] },
	},
	onError,
}, ctx);

createPlugins is the builder, and it builds for every kind, because only the server has the sources. For each kind it needs two facts. The platform says where the scripts run: a node script may import node:fs, a browser script may import a stylesheet but not node:fs. The host package names say what to leave out; they differ per kind, because a tab also shares React with its plugins and a worker does not. Nothing here runs a plugin.

Evaluating: plugins.load, in the process that hosts

Evaluating is where the names become things. When the script runs and asks for @app/core, someone has to hand it the actual module, the one this process imported when it started. Only the process itself has it:

// server.ts
import * as chord from "@earendil-works/chord/next";
import * as core from "./core.ts";                         // the app's base code, the same module plugins import as "@app/core"

const report = await plugins.load(server, {
	kind: "server",                                          // evaluate the server kind's scripts
	modules: { "@earendil-works/chord/next": chord, "@app/core": core },   // this process's own instances
	core: [{ name: "@app/core", facet: core.serverFacet }],  // base-code facets, loaded beside the plugins
}, ctx);

modules is this process's answer to every host package name. The server passes its own; a worker passes its own in workerMain; a page passes its own in startPage. Each process has different instances, which is exactly the point: inside each process, plugins share that process's Chord and base code.

core is the app's base code in facet form. It is a facet like a plugin's, but it is not a plugin: it is made in the process's own code, often from values only this process has (core.workerFacet(start) closes over a worker's start data and store), and it never reloads, because it defines what all plugins share. Passing it here loads it together with the plugins, so they are published in one step.

Why plugins.load(host), not host.load

A host has its own load, and plugins.load calls it. The host's load takes facets that already exist: functions in memory. It knows nothing about directories, builds, scripts or modules, on purpose: the same host runs in a test with facets imported directly, or in a Cloudflare Durable Object where plugins are bundled at deploy time and nothing is built or evaluated at runtime.

The plugins component is the part that knows about directories. It finds the plugins, builds them, evaluates them with the process's modules, and then calls host.load with the result. It also remembers, per host, which facet each plugin runs, so that a later reload can hand the host its complete new plugin set, with unchanged plugins as the very same objects. That memory is why the host is passed to it: plugins.load(server, …) on the server, plugins.load(worker, …) in a worker (where workerMain gives it a plugins that only evaluates), the same call in every process.

SERVER: BUILD (ONCE PER EDIT)EACH WORKER: EVALUATEplugin sourcesnotes/worker.ts`, imports`@app/corecreatePluginscopies own files and npm deps; leaveshost packages outworker script`require("@app/core")` left as a namethe worker's modulesits own `@app/core`, its own Chordplugins.load(worker, …)runs the script, answers each namethe facetuses the worker's `Session`, theworker's `defineFacet`shipped
Figure 3. Building on the server needs names; evaluating in each process needs that process's modules.

What happens, step by step

plugins.load(server, { kind: "server", … })

Discover

The component lists ./plugins one level deep and reads each package.json. Its name is the plugin's name; the files named after a kind are its facets. A directory without a package.json, or with two server files, is reported, not loaded.

Build

For each plugin with a server facet it builds one script, if no current one exists. A syntax error or an import the rules do not allow (a path into base code, a package not in the plugin's dependencies) is a failed build with the file and line, never a crash.

Evaluate

The script runs. Each host package name it asks for is answered from modules; anything else it may not have. The script's default export must be a facet.

Load

host.load runs the setups of core and of every plugin facet and publishes them together. A plugin whose build, evaluation or setup failed is left out and listed in report.failed with the reason; the others load. Objects that need it wait until a later edit loads it.

Only the server builds. Workers and clients (browser tabs, terminal UIs) only evaluate: the server builds their kinds too, a worker gets its scripts in its start message, a tab or a TUI gets them from the server over HTTP, and each evaluates them with its own modules (a worker's from workerMain, a page's from its base bundle).

VII

Plugins Using Plugins

Two ways one plugin uses another, and the one it may not.

A search plugin wants to search the notes. It imports the contract and asks for the object:

// plugins/search/worker.ts
import { Notes } from "@app/notes/contract";    // another plugin, by its name: copied into search's script

host.serve(Search, ({ project }, principal, self) => {
	const notes = self.get(Notes, { project });   // a dependency: Search goes live once Notes is
	const index = self.derive([notes.list], ([list]) => buildIndex(list.items));
	return { variant: "main", members: { find: async ({ q }) => lookup(index.value, q) } };
});

A plugin imports another plugin by its name, never by a path: @app/notes/contract is the file contract.ts of whichever discovered plugin is named @app/notes, in any root. The file is copied into the importer's script, so only contracts and other side-effect-free files belong there; a missing plugin is a build error naming it.

self.get in a factory declares a dependency: Chord makes Search live only once Notes is, and if the notes plugin is missing, Search waits with a clear reason ("waiting for notes: not served here") instead of failing.

The other way is a registry: an extension point one plugin defines and others contribute to. The app's core defines Panels, and every plugin with a UI contributes one:

// plugins/notes/web.tsx
export default defineFacet((host, self) => self.contribute(Panels, { title: "Notes", Component: NotesPanel }));

What a plugin may not do is reach into another's data. Search never reads notes' documents; it uses the Notes object's state. That rule is what lets the notes plugin change its storage without breaking search.

VIII

Changing a Plugin

Edit, build, roll out: how new code takes over while every tab stays connected.

You change the notes plugin: notes now have a timestamp. The watcher sees the saved file and marks the plugin dirty. You (or an agent) build and roll out:

const built = await plugins.build(plugins.state.value.dirty, ctx);
const result = await plugins.rollout([plugins.step("server", server), workers.step("workers")], built, ctx);

The rollout goes host by host, server first, then every running worker. On each host Chord compares the new plugin set with the loaded one, runs the new factories against a recording self to see what each object would declare, and then gives every object of the changed plugin its fate:

The object What happens
holds no natives (open files, sockets), the new code neither swapped in place: new methods, same documents; running calls finish on the old code
holds natives restarted: calls wait, running ones finish, natives close and reopen with the new code
its type was removed ended; its users wait until something serves it again

The tab's reference follows the new object and gets one frame with the difference. Nothing reconnects. If the edit is broken (a syntax error, a factory that throws), that plugin keeps its old code on that host and the report says why; every other plugin of the rollout applies.

Versions

A change to a contract (a new field in Notes's state) gets a new version. A tab built with the old contract keeps working if the provider maps the old version (accepts); otherwise it waits with "version mismatch" until it reloads.

IX

Base Code and Plugins

What goes into the app itself, what goes into plugins, and how an app ships plugins of its own.

Every app has two kinds of code, and the line between them decides what can change while the app runs.

Base code is the app's own program: src/, started by node server.ts. It is what every plugin shares: the host kinds (Server, Project), the principal type, the app's defineFacet, the registries plugins contribute to (Panels), and the startup that creates hosts and loads plugins. Plugins import it as a host package (@app/core), so inside one process there is exactly one of each. Base code changes only with a restart: if it reloaded, the process would hold two Panels registries, and old and new plugins would no longer meet.

Plugins are features: notes, search, export, a terminal. They reload on an edit, a broken one is left out alone, and an agent can write one while the app runs.

The rule: base code is what everything shares, not features. A feature in base code cannot be edited live, and a bug in it needs a restart. Keep base code to host kinds, principals, shared registries and startup.

Plugins the app ships

An app usually comes with plugins of its own: notes, search, settings. They are still plugins, kept in a root inside the app, next to the root where users and agents add theirs:

const plugins = createPlugins({
	roots: ["./src/plugins", join(dataDir, "plugins")],   // the app's own plugins, then the user's
	kinds,
	onError,
}, ctx);

They are built at startup like any plugin (a quarter of a second for twenty), reload like any plugin, and are left out alone when broken. If two roots have a plugin with the same name, the first root wins, and the other is reported.

Subpaths of host packages

A host package is matched by its exact name. If base code offers several entry points (@app/core for every host, @app/core/web for pages), each is its own host package, listed by name for the build and given as a module when evaluating, as react and react/jsx-runtime are. A plugin importing a subpath that is not listed gets a build error naming it.

What the app kit will change

The app kit being designed next (chord/next/app) will make the base code nearly empty: startApp and startWorker with Chord's conventions for hosts, users, authentication and storage. An app is then its plugins, its own ones in src/plugins and the user's beside them.

A

Reference

Call What it does
createHost<P>({ address?, principal?, store?, onError }) a host: addressable with an address, else a client
host.load([{ name, facet }], ctx) / host.reload(...) publish facets / apply a new plugin set
host.get(Type, params, ctx) / host.hold(...) a live reference / a reference at once that follows its object
local(name, { version, params, members }) / remote(name, { version, params, hostAddress, variants }) contracts
defineFacet((host, self) => …), host.serve(Type, factory), self.contribute(R, value) a facet
factory self: state, derive, get, registry, open, on, onLive, job, tx an object's parts
createPlugins({ roots, kinds, onError }, ctx) the plugins component
plugins.load(host, { kind, core, modules }, ctx), build(names), rollout(steps, built) load, build, roll out
hostKind(name, params?), Kind(params) host kinds and addresses

Hosts and Plugins · first edition · 2026-10-31 · Chord v2 slices 1-10