# Slice 13: the app kit (spec) Status: **approved** (owner, 2026-11-01: decisions "The app kit, point 1" to "point 5", then "go ahead, just do all the things"). Builds on slices 10 (`http`, `workers`) and 12 (the plugin model). `chord/app` in `src/node/app/`: an opinionated layer so an app is its API, its built-in plugins and a start script. Read in full before writing code: this spec; `decisions.md` entries of 2026-10-31 and 2026-11-01 (the app kit, the plugin model, "The app kit is opinionated", "Settings are arguments, extensions are registries", "The HTTP component's security baseline"); `design.md` §3, §4, §10, §21, §23 (all of it, "The app kit" included); `slices.md` (carry-over "Slice 13"); `slice-10.md`, `slice-12.md`; the READMEs of `node/http`, `workers`, `plugins`; p2's `src/runtime/server.ts`, `src/hosts/server.ts`, `src/hosts/worker.ts`, `src/runtime/loopback-cookie.ts`, `src/hosts/principals.ts`, `plugins/devices/`, `plugins/sharing/`, `plugins/files/` for what exists. ## 1. `startApp` and `startWorker` ```ts // start.ts (the app's start script) const app = await startApp
({ name: "acme", // the default data directory: ~/.acme dataDir: process.argv[2], // optional; no environment variables listen: { host: "127.0.0.1", port: 8788 }, api: { name: "@acme/app", versions: ["v3"], modules: { "@acme/app/v3": apiV3, "@acme/app/v3/server": apiV3Server } }, kinds: { server: { platform: "node" }, worker: { platform: "node" }, web: { platform: "browser", packages: { react, "react/jsx-runtime": jsx } } }, principal: { server: { kind: "server" }, worker: (address) => ({ kind: "worker", key: address.key }) }, workers: [{ kind: Project, // a host kind the app defines, any params entry: "./worker.ts", start: (address, ctx) => ({ id: address.params.id }), authorize: (principal, address) => mayReach(principal, address), idleMs: () => 5 * 60_000, }], onError: (error) => log(error), }, ctx); // app.host, app.http, app.workers, app.plugins, app.applyChanges(ctx), app.exportAddress(address, ctx), // app.importAddress(archive, address, ctx), app.ownerLink(), app.close(ctx) ``` ```ts // worker.ts await startWorker
({ api, kinds, onError }); // reads the start message, opens the worker's store, loads plugins
```
- `startApp` creates the data directory layout (§4), opens the server store, creates the host (address `Server()`,
the server principal), `createHttp` (settings as arguments), `createWorkers` per worker kind (`kind` named after the
host kind; the worker's facet kind is `worker` unless the entry says otherwise), `createPlugins` with the roots of §5
and the app's API (slice 12), loads the server's plugins with the kit's own facets (§2, §3, §4) as core, and resolves
once loaded. `close` closes in the right order (http, workers, host, store).
- `startWorker` is `workerMain` plus: the worker's directory and store under its kind and address, the host with the
principal from the start message, `connect` first, plugins loaded with the kit's worker facets, `busy` from the app
if given.
- The start-data facet: the kit serves `AppInfo` (data directory, app name, version) on the server and `WorkerInfo`
(address, start data) on workers as core objects plugins `get`.
- Zero worker kinds: no workers component.
## 2. Principals and sessions
- The app's principal type `P`; the kit needs only `principal.server` and `principal.worker(address)`. The kit adds
one variant for its owner: `{ kind: "owner" }` must be assignable to `P` (a type check).
- **Sessions** (`app/sessions.ts`, a kit object on the server, persisted in the server store): `issue(principal,
{ label, kind: "browser" | "device" }, ctx)` returns a token; tokens stored hashed; `revoke(id)`; `list` (watchable:
label, kind, created, last used). The kit's authenticator (contributed to `Authenticators`) reads the session cookie
(signed, `HttpOnly`, `SameSite=Strict`) or an `Authorization: Bearer` header, and gives its principal.
- **Owner link:** at start the kit issues a one-time token (random, single use, valid 10 minutes, replaced at each
start) and `app.ownerLink()` returns `http://127.0.0.1:8788/_chord/owner#`, owner only, a confirm page); the client polls
and receives a device token. Codes expire after 10 minutes.
- **Deny by default** (an HTTP component change): a route or socket without a principal is refused (401) before plugin
code, unless its contribution marks it `public: true` (routes: `{ handler, public: true }`; sockets likewise). The
`/chord` links endpoint requires a principal. Foreign requests stay anonymous as before, so they reach only public
routes. Slice 10's examples and tests mark their anonymous routes public.
## 3. Optional kit plugins
Shipped as plugins (in `packages/chord/plugins/` or exported facets the app adds to its root; choose one and say
why): `users` (accounts keyed by id, a profile schema the app passes in, `self.get(Users, {})` with `get`, `update`,
`list`), `invite` (invite links issuing a session for a principal the owner chooses), `ephemeral-login` (a public
route taking a name, issuing a session for `{ kind: "user", id: