# Moss — Design Document

## Purpose

A lightweight runtime that serves *capabilities* instead of code. A principal
logs in, a charter resolves, and what arrives over the socket is their
application: the actions they may use, the data those actions may touch, live,
revocable, and identical in mechanism for humans and AI agents.

**One sentence:** the resolved catalog is the application.

**Why it had to exist.** The stack was server-shaped and had no server. Actions
are wire-ready data and nothing wired them; the charter resolved but had nowhere
trusted to execute; vex's multi-user machinery was built and dormant; server
functions had no host; there was no transport anywhere. Six mechanisms, all
built or specified, all homeless — and every app rebuilt the same wiring (its
own shell, its own vex host, its own identity stub). Moss is the missing organ:
the trusted place where the charter executes, the shell runs, and existence is
served.

---

## The inversion

A traditional application server hosts code and exposes endpoints. Everyone
receives the same application; authorization is a wall of checks around it. Moss
inverts the relationship on four axes.

### 1. It serves existence

The unit served is the action — the complete behavioral contract (layout +
triggers + endpoints + input). A principal's resolved catalog **is** their
application. Deny-by-nonexistence, made true by a server rather than cosmetic in
a client: an ungranted action is not hidden or disabled, it is absent. There is
no `if (canAccess)` to forget, because the thing to guard was never sent. The
warehouse kiosk granted two actions is not a locked-down app; it is a two-action
app.

### 2. Deployment is a data operation

Every field of the manifest is an authored artifact — the charter, the action
definitions, the layout variants, the entries that are its API surface, the
shell's canvases. Publishing a change is a write to that data; moss derives the rest.
Consequences:

- **No version skew.** The shell and the definitions it runs live server-side;
  the client is a thin terminal that rarely changes.
- **Rollback is a pointer move.** The library is versioned; "published" is a
  pointer per (artifact, tenant, channel), and reverting moves it back.
- **The release artifact is a diff, not a build log.** Because permissions and
  UI are closed data, a publish can render its consequences in concrete terms —
  *"crm.quotes → gained by: sales, intern, ray."* A human approves that.

Today the manifest is what the app handed `createServer`: read by a schema,
then frozen, so an edit to it throws. Two parts of it change while a server
runs, each through one door: a set of actions is handed over
(`server.replaceActions`), a set of entries (`server.replaceEntries`), and
each is verified before it is taken. The artifact library
that makes publish a runtime write for every artifact is specified and unbuilt
(see below). The mechanism is already the function it needs to be: what a set
of roles holds, and the policy it reads and writes under, are derived from what
is in force and dropped when that changes, so there is no invalidation
protocol; a pointer in the database tells every other process that it did.
The pointer tells them *that*, not *what*: another process reads again the
sets of actions it has a loader for (the ones a feature keeps in rows), and a
set handed to one process with `replaceActions` or `replaceEntries` and no
loader is that process's alone. A library every process reads is the part that is unbuilt.

And there is one place it will be asked. Everything a shell loads that
somebody authored (its actions, its frame and layout store, its fragments, the
entries its reads replay) is answered by the running manifest in one function,
for a shell and a principal with their roles (`artifactsFor`, `src/artifactory/running.ts`). A shell is built
from that answer and adopts it: an adopt puts in place the actions that are
held now, and each `{ ref }` of the layout store and each fragment that the
answer says differently from the last time. Every layout the shell draws is
one of those refs, under the name of what draws it: the answer holds an
action's in the layout store under `action/<id>`, and the definition names
it; the shell's own is under `frame` and a canvas's under `canvas/<id>`
(`shellWithStoredLayouts`, `src/artifactory/artifacts.ts`). So a layout is changed by setting
a ref, which a screen that is open follows with its data kept, and not by
registering the action again, which only a screen opened afterwards sees. It
is also how a saved layout reaches a screen: under an action's ref the answer
holds the principal's own layout that is marked in use, else the layout
variant their roles hold, else the one published for everybody, else the
build's. The first two tables of the
library are built for that: a row for each save and a row for each version it
holds, never changed (`src/artifactory/saves.ts`). A row is its principal's,
and vex keeps it so. What is published for everybody is the rows of one
principal no caller is, under that same rule: publishing is the save a person
makes, made by the app's own server code as that principal
(`server.defaults`), and no second mechanism. An action or a fragment
somebody generated is a row of theirs too: it is added to what their shell is
built from, under an id moss gave it, and it may only replay reads its owner
may replay (`src/artifactory/generated.ts`). A layout saved under `frame` or
`canvas/<id>` takes the place of that layout of the app's own shell, a
person's own over the published one over the build's; a page draws the ones
it declares. (The
canvases a manifest declares are what a shell starts with: an adopt does not
take them again, a reset does. Nova gives a shell a canvas when something is
first pushed onto it; an adopt takes out one the manifest does not declare
once no layout in force holds a slot for it, where one did: `slottedCanvases`,
`src/artifactory/artifacts.ts`.) The
entries vex replays it asks the running manifest for as well, before its own
cache table; nothing else in moss reads one of the four off what the app
handed over.

### 3. One gate, every kind of principal

The resolved catalog feeds the human's shell, an agent's tool policy, the
function gate, and the boot verifier from **one document**. User permissions, AI
guardrails, feature flags, and plan entitlements collapse into one charter
resolving to one set, per principal.

### 4. The shell runs on the server

Nova's core is headless and its interchange is serializable (`RenderNode[]`
down, `NovaEvent` up), so the shell executes server-side and the client is a
canvas terminal: a registry, a socket, a renderer. State is authoritative — the
LiveView move, but streaming semantic trees, so the terminal is tiny and
swappable — five targets ship: the app's React kit (`terminal/react`), the
app's Vue kit (`terminal/vue`), nova's plain-DOM kit (`terminal/dom`), a line REPL (`terminal/tty`), and a
full-screen TUI (`terminal/ink`). Swappable is literal: `mountTerminal`
hot-swaps targets over one wire, the session and current trees surviving the
swap. The DOM target is the proof the terminal is trivial — nova's DOM adapter
and default kit, zero framework, zero config.

The cost is the round trip, and two rules cover the one interaction that cannot
wait for it: the terminal never overwrites the value of the focused input when a
new tree arrives (bound inputs are identified by `model.path`), and `ui:model`
events debounce. A canvas — or a whole app — may also opt *out* and run its shell
in the browser, with vex as the enforcement floor: this is where offline and
zero-backend PGlite live. It is an explicit degrade, not a mode menu. What is
built is the whole app: an app with its own shell, which moss does not serve.
One screen mixing client-side canvases with authoritative server ones —
**authority islands** — is what the design allows, because a canvas slot
renders a tree and is indifferent to whether it came from a local shell or a
socket; no render target of moss's does it today, each registers its slots
against the wire alone (`src/terminal/targets.ts`).

---

## Derivation over configuration

The organizing principle: an app hands over **artifacts** and an
**environment**, and everything mechanical is *derived* — never configured.

- The **data layer** stands up from what's present: the app's entries are the
  API surface (nothing is generated), and the grantable set is the
  introspected schema × vex's verb leaves. No table list is authored.
- **Per-principal resolution** is the charter applied at login: roles from the
  app's `identity` seam or its assignment rows, granted action ids (with a
  content-hash version token), the compiled vex scope policy, and the granted
  layout variants (ring 2). Pure; kept for each set of roles, not for each
  person, until what the app consists of changes.
- The **component registry** on the server is name-only stubs, derived by
  walking what mentions a component: the actions in force, the layout
  variants, the shell's canvases, frame, layout store and fragments
  (nova/reflect's `componentsOf` — the walk is shared, not re-derived) — the
  actual components live in the terminal.
- **Coherence is refused, not documented.** Whether an app may be served is
  one verification (`src/manifest/verify.ts`): every action is one nova reads,
  the charter verifies (`verifyCharter`, with nova's closure audit over the
  definitions in force), every entry's `reach` names a profile the behaviors
  declare, and nobody holds two layout variants of one action. It is asked at
  boot, at every change (`refresh`, `replaceActions`), and of a manifest that
  is not served (`verifyApp`); a boot or a change that fails it throws. The closure audit's findings are not errors: they are
  filed under each role's `issues` in the report, and boot neither refuses on
  them nor prints them. If it boots, the charter and the variants are
  coherent; a push to an ungranted action can still be in it.

The test of the principle: moss's core adds no new vocabulary. Reads are vex,
writes are vex, compute is functions, UI is actions, permissions are the
charter. The server is the composition, not a fifth language. (The integrations
feature does read a document of its own, the bundle a separate service
publishes about itself. That is one reason it is a feature, and not the core.
Of a bundle it reads the fields that are nisc's own: the integration's id, what
it asks to be allowed, its grammar stamps, its actions and its phrases. What a
product adds to those (a store listing, where a screen is placed, a document a
person edits) is the host app's vocabulary: a bundle carries it under
`extension`, the app supplies the schema that reads it, and moss keeps it
without reading inside. A package does not own a closed vocabulary for what is
open.)

### Ring 2 — served layout variants

Per-principal UI difference is existence (ring 1) or a served variant (ring 2),
never a runtime capability flag in a layout. A layout variant (`NiscApp.layouts`)
is `{ action, layout }` under a minted id; the charter's `layouts` section
selects who holds which. At shell build, moss puts the granted variant's
layout where that action's layout is kept *before* the shell exists, so every downstream render
and serialize is already per-principal and the terminal renders what it is served
— nothing on the wire says why a button is absent. The base layout is the floor
(the least-privileged holder's shape); variants enrich upward as grants, so
`extends` composes them and a forgotten grant fails closed. This is the charter's
extensibility paying off: one `Section` value, moss hands the universe in and
compiles it.

---

## How the code is laid out

Moss answers five questions about one request, in this order, and each
question has one module that owns it. A module is a directory under `src/`
with an `index.ts`, and nothing else sits at the top of `src/` but the main
entry.

1. **Who is calling?** `identity`: a token is a principal (the deployment's
   verifier, moss's stored credential, the browser's cookie and its seal), and
   a principal is a record (roles, scope values, what is installed for them).
2. **What exists for them?** `artifactory`: what the app consists of right
   now, with one writer, and what a set of roles holds of it. Every artifact a
   shell is handed is asked of it: the actions, the frame and the layout
   store, the fragments and the entries, in one answer for a shell and a
   principal, and read off the app by nothing else. It keeps what somebody
   saved while the app runs (a layout, today) and answers it in the same
   place. It is the one holder of the
   entries too: the ones the app shipped and each set handed over, compiled as
   vex replays them. It stands on `manifest`,
   the app as it was handed over: the types an app is written in, the schema
   it is read by and frozen under, and the one verification of whether what
   is in force may be served.
3. **What may they read and write?** `data`: the vex engine, the policy a set
   of roles reads and writes under, the routes, an entry run as a charter
   role, and the tables with their migration. The engine's cache asks the
   artifactory for a fingerprint first, through two functions the server
   hands it, and its own table (`vex_cache`) for what the artifactory does not
   hold: a query written for an intent while the app runs. Nothing is copied
   into that table, and a copy an earlier release left in it is not replayed.
   So what a fingerprint resolves to is decided in `artifactory`, as what an
   action, a layout or a fragment resolves to is.
4. **What are they looking at?** `shells`: the shell host, one shell's build,
   the stream of frames, and the screen read without attaching.
5. **How does it reach them?** `socket` for the live wire, `documents` for a
   screen drawn into a page.

Under them sit two modules every other may import: `runtime`, what a
deployment supplies (the database, the verifier, the limits, the telemetry
sink), and `protocol`, what both ends of the wire agree on (the messages, the
close codes, the delta codec, the snapshot a drawn page carries). Over them
sits `server`, which answers none of the five: `createServer` constructs the
modules, hands each the narrow things it needs, starts the features and mounts
the routes, one step to a line. Over `server` sits `node`, how moss runs on
Node: the listener, the `ws` transport, the built site, and the Vite dev plugin
(`@niscorp/moss/node` and `/vite`). It imports the server, and nothing imports
it.

A **feature** owns none of the five. It lives in a directory of its own, ships
from an entry point of its own, and the core never imports it: the app creates
it and names it in its manifest's `features`, and the server starts it with
the seams (`FeatureSeams`: every member one function or one narrow value of
the module that owns it). `Feature` and `FeatureSeams` are the contract between
moss and its two features, and are not exported: an app names a feature and
cannot write one. **The seams are made for the feature they are handed
to, and what they reach is that feature's**: its own tables, which it names on
itself before it runs (`Feature.tables.names`); its own set of actions, kept
under its name, which it alone writes; the documents in its own rows. The core reads and
writes no row of a feature, reads no header of one, and holds no setting of
one (the key the integrations feature signs with is that feature's own).
`feature` is the module that says what a feature and
its seams are: types, and no code. There are two features: `integrations`
(`@niscorp/moss/integrations`), and `tide` (`@niscorp/moss/tide`), a committed
write minted into tide as a fact. A feature that needs something the seams do
not offer does not reach for it: the seam is added to the module that owns
the answer first, as a change of its own.

The terminal side is two more modules, built and shipped apart from the
server: `client`, the wire, which imports `protocol` and nothing else of moss,
and `terminal`, the conductor and the render targets, which imports `client`.

Which module may import which is written down once, as a table a build reads:
`scripts/check-modules.mts` in the repository. That table is the truth, and
this is a copy of it:

| Module | May import |
|---|---|
| `runtime` | nothing of moss |
| `protocol` | nothing of moss |
| `feature` | `runtime` |
| `identity` | `runtime`, `protocol` |
| `manifest` | `identity`, `feature` |
| `artifactory` | `identity`, `manifest` |
| `data` | `runtime`, `identity`, `manifest` |
| `shells` | `runtime`, `protocol`, `manifest`, `data` |
| `socket` | `runtime`, `protocol`, `identity`, `shells` |
| `documents` | `protocol`, `identity`, `shells` |
| `server` | every module above, and no feature |
| `node` | `runtime`, `protocol`, `identity`, `manifest`, `socket`, `documents`, `server` |
| `integrations` (a feature) | `runtime`, `feature` |
| `tide` (a feature) | `feature` |
| `client` | `protocol` |
| `terminal` | `client` |

The table has no cycle, and what it leaves out is as deliberate as what it
names. `shells` does not import `identity`: who somebody is, it is told.
`socket` does not import `artifactory`: what somebody holds is a function it
is handed. `artifactory` and `data` do not import each other: the generation
pointer's row is `data`'s, when it moves is the artifactory's, the entries
over the artifactory's two tables are written by the artifactory and replayed
by `data`, and each reaches the other as functions the server hands over. `integrations` does not
import `data`: what it reads and writes it
authors as vex entries and replays through a seam. Nor `manifest`: what it
asks about a bundle before letting it in (does a layout fit the kit, which
chains of steps never end) it asks through two seams. A feature imports `feature`, and `runtime` for where a span
goes, and nothing else of moss. A module imports another by its `index.ts`
and never a file inside it.

Four rules go with the table. A module's factory is handed a typed object of
the functions and values it needs, never the whole manifest and never the
whole runtime: those two are handed to `createServer`, and the wiring hands on
the parts. Each table and each long-lived piece of state has one owner, and
nobody else authors what is written to it. What crosses a boundary (a
manifest, a socket message, a route's body, a row, a token) is parsed by a
schema in the module where it enters, and no module holds a type assertion.
And a function is at most 300 lines (200 is fine; 300 is the ceiling), a file
at most 400 lines of code, a line at most 200 wide.

**One owner, as the code has it.** The owner of a table authors every
statement on its rows. It is not always the module that runs them, or the one
that made the table:

| Table | Whose | Where its statements are written | What is somewhere else, and why |
|---|---|---|---|
| `sessions` | `identity` | `identity/sessions.ts`: five statements straight to the pool, before anybody is signed in | Its table is made in `data`'s one migration run, from a sequence `identity` declares |
| `moss_generation` | `data` | `data/own-entries.ts`: two entries, one that reads and one that moves | `artifactory` decides when it moves (`artifactory/generation.ts`), through a function `data` hands over |
| `artifact_saves`, `artifacts` | `artifactory` | `artifactory/saves.ts`: three vex entries and the row rule they run under, replayed through a function `data` hands over (`Data.artifactEntries`) | The two tables are made by the core's sequence (`data/tables.ts`, `nisc.moss/5`). Their rows are somebody's, unlike the pointer's: the entries are replayed with the caller's scope, and vex keeps every read and write to the rows whose `principal` is the caller's `userId`. What is published for everybody is the rows of the principal `*`, replayed with that as the `userId` by the server's own code and by no caller. Moss updates and deletes no row, so a stored body is not rewritten when a grammar moves: it is brought to the current grammars where it is read |
| `integrations`, `integration_actions` | the `integrations` feature | `integrations/entries.ts`: vex entries, replayed through the seam the feature is handed | The two tables are made by the core's own sequence (`data/tables.ts`, `nisc.moss`): that is the history every database has run, and it is not edited. The documents stored in `integration_actions` are brought to the current grammars by `data`, which holds the pool, when the feature asks through its seam |

`data` holds the engine and the one ledgered migration run, so every owner's
entries and every owner's migrations run through it. It authors no statement
on a table that is not its own. The app's own tables, and tide's when the app
keeps them, are migrated in the same run (`runtime.tables`) and are the app's
and tide's.

What holds the rules is a build that fails, not this page: `pnpm
check:modules` (the table above, the doors, nothing flat, no cycle, and no
feature in the core), `pnpm check:api` (an exported name is a line in
`api.txt`, and a line there is an exported name), `pnpm check:raw-sql` (a
statement outside vex is counted, with its reason) and `pnpm lint` (no type
assertion, no `any` that becomes a typed value, no written type predicate, the
two sizes and the width).
**Narrow inputs and one owner are held by none of the four**: they are read
for in review, and the table above is what a review found, not what a build
checks. What each gate holds is said at the top of its script. The rules
as somebody changing a package follows them are in the application guide,
"Changing a package" (`packages/nisc/AGENTS.md`).

---

## The socket

The authority channel. Transport-blind: the protocol speaks through a
four-function `Connection` seam (`send`/`close`/`onMessage`/`onClose`, and what
the upgrade request offered), and the RFC 6455 plumbing lives with each
runtime's entry (`ws` on Node in `./node`, Bun-native later). Who a terminal is
is decided on that request, from its headers — a session cookie its browser
sent, or a token it offers as a subprotocol beside `nisc` — and never from its
address, which every proxy on the way logs. One connection per client carries every canvas — ten open
canvases are ten canvas ids on one pipe.

**Down:** `hello` (the resolved catalog on connect, and the protocol the server speaks — the terminal names its own on the upgrade), `catalog` (declared for
catalog pushes — not yet sent; terminals know it and ignore it), `frame` (the
canvas arrangement — a served layout of `CanvasSlot` markers), `render` (a
canvas's tree), `render-delta` (the same tree, against the last one — opt-in,
see below), `session` (a login grant), `error`. **Up:** `event` (a
canvas-tagged `NovaEvent`), `publish` (a channel message), `resync` (this
connection's copy of a canvas drifted; send whole frames), `back` (undo the
last navigation), `popTo` (jump to an instance already on a canvas's stack),
`reset` (below).

Operational shape, all falling out of "the message is state, not history":

- **Backpressure.** At most one pending tree per canvas per connection; a newer
  render replaces it, never queues behind it. A diff *stream* could never drop
  an update this way — which is exactly why frame deltas are written against
  the last frame **sent**, never against an intermediate that was coalesced
  away. Dropping renders stays free.
- **Delivery.** Events are fire-and-forget; the tree is the confirmation. No
  acks, no replay after reconnect — a replayed intention against a changed screen
  is worse than a lost one, so reconnect simply re-sends the current trees. The
  same rule holds before the first connect: nothing is sent on a socket that is
  not open and nothing is held for when it is. A server-drawn page (below) is on
  screen before its socket, so a press in that moment is dropped.
- **Durability.** The database is the durable thing; a shell is a warm cache,
  rebuilt on the next connection from definitions and whatever the app reads
  from rows as it builds. Evicting an idle shell, restarting the process and
  `reset` cost the same and nothing more: what was only in the shell — the
  screens someone had opened, and what they had typed. This is why an idle
  sweep can exist at all.
- **Recovery.** `reset` (up) throws a session's shell away and serves its
  replacement. It names no canvas, because it is the recovery for a session
  whose canvases are the broken thing; it is protocol-level, so no action
  declares it and no charter grants it. This is not a convenience: a shell is
  server state keyed by principal, so a wedged one survives every gesture a
  client can make alone — reloading reattaches to it, and so does signing out
  and back in. Without a verb the client can send, a person watching a dead
  screen has no move left.
- **Going back.** `back` (up) undoes the last navigation. It names no canvas
  either, and for a related reason: back is one gesture over the whole shell,
  and a terminal is served trees, not stacks — it could not name the canvas that
  moved last if it wanted to. What back MEANS is decided where the stacks live:
  nova's shell keeps a journal of the positions its navigations displaced, and
  `back` restores the newest one it can. So the browser's back button, a TUI's
  Escape and an app's own control are one message, an app authors nothing to
  receive it, and the terminal holds no location that could drift out of step
  with the server's. It is also the answer to a real failure: with no history
  integration at all, a browser's back button unloaded the application.
- **A connection that drops.** The trees a terminal holds stay on screen
  while it reconnects: the current ones are re-sent when it does, and blanking
  in between would show a working app as an empty one. The price is that a
  screen left up by a dead socket looks exactly like a live one, so the
  connection's status is on what every render target renders against, beside
  the frame and the trees, and a change of it is an update like a change of
  them. What is drawn for it is not moss's: the two console targets print a
  line, and the three that draw into an element say it on that element
  (`data-wire-status`) and leave what it looks like to the app's stylesheet.
  The terminal holds one socket at a time — it lets go of the one it had
  before it makes another — so one connection writes the screen, whatever a
  person pressed while the last was still connecting.
- **Emptiness.** A canvas whose layout renders no visible content is served as
  `[]`, so a terminal collapses chrome on `length` alone. An `ActionSlot` is a
  boundary, not content — visibility is decided by what's inside it.
- **Size.** Two optional reductions — transport compression and frame deltas —
  sit under the protocol, both off the app's books. See below.

### Where a browser keeps its session

A terminal served by the app it talks to does not hold its session. Its browser
does, in a cookie no script can read, and sends it by itself with the two
requests that need to know who is asking: the page, and the upgrade that opens
the socket. A token in the page — in storage, in a cookie script can read — is
there for anything else that runs in the page to take; one the page never has
cannot be taken from it.

- **The answer to the upgrade is where the cookie is written.** A cookie is set
  by an HTTP response and by nothing else, and the upgrade is the one request
  every terminal already makes. So who is asking is decided *before* the
  request is answered, and the answer carries the cookie.
- **A sign-in over the socket reaches it sealed.** `session.grant` happens on a
  socket, which cannot write a cookie; the token has to travel through the
  page to the next upgrade. It travels encrypted with a key the browser holds
  in a second cookie no script can read or set — the *seal*, given by the
  answer to the browser's first upgrade under a name (`__Http-`) a browser
  lets only an HTTP answer set. The page can hand a sealed sign-in back and
  cannot open it, and neither can anybody it is shown to: without that
  browser's seal it opens nothing. Nothing is kept here between the two
  requests, so it works whichever process answers the second, and it works
  for any credential — moss's own, or an app's provider's. The seal rides only
  the socket's path, so no page request carries it and a page drawn for nobody
  is still the same for everybody.
- **Only for the app's own page.** A browser sends a cookie with any page's
  request to this host, a stranger's included, and the socket is the whole
  app. So the cookies are read and written only when the request's `Origin` is
  the host it was addressed to (or one `runtime.origins` lists, behind a proxy
  that rewrites `Host`). From anywhere else a terminal is who the token it
  offers says, or nobody.
- **No route answers to it.** The HTTP surfaces read `Authorization` and
  nothing else, so no request another site can make a browser send does
  anything as the person. A page request does read it — every site may link to
  a page, and none can read the answer.
- **Its name is the terminal's token key**, so several people can be signed in
  on one origin, each in a seat. It ends with the port when the page is on
  one: a browser keeps cookies by host, not by port, and two apps on localhost
  would sign each other out. Over https it is `__Host-` prefixed and `Secure`,
  which a browser lets nothing but this host set.
- **What the page sees is a flag** (`<name>.held=1`), with no secret in it: how
  a terminal knows to let go of a token it was holding, and that a page drawn
  for somebody was drawn for it.
- **Signing out says so.** moss revokes its own credential, and a cookie that
  no longer resolves is taken back on the next upgrade. An app's own provider's
  token may go on resolving, so a terminal that was signed out says it is
  leaving, and the cookie is taken back whatever it holds.
- **Any other terminal holds its own token** — a process, a page served from
  somewhere else, a browser that refuses cookies — offers it, and is handed a
  sign-in as its token, as before. A token such a page of the app's own offers
  (one an earlier build left in storage) is moved into the cookie and let go of.

### What a frame costs

A server-authored shell pays for its inversion in bytes: every change re-sends
a canvas's whole tree, where a client-side framework would have re-rendered
locally and sent nothing. That is the trade the shell exists to make, and it is
worth measuring rather than assuming.

Two reductions are available, and they are deliberately separate things:

- **Compression** (`socketCompression`, **on** by default) is
  `permessage-deflate`, an RFC 7692 websocket extension. It is negotiated by
  the transport, invisible above the `Connection` seam, and understood by every
  browser. Render trees are mostly repeated keys, so it earns roughly 3–4×. Its
  cost is memory, and it is not small: about 260 KB resident per connection at
  the `ws` defaults, against about 43 KB for the shell being served. That trade
  is obviously right at hundreds of connections and obviously wrong at a
  hundred thousand, which is the only reason it is a knob rather than a
  constant.
- **Frame deltas** (`shellFrameDelta`, **off** by default) describe a changed
  canvas against the frame the connection already holds, as copy runs plus
  literal inserts (`src/protocol/delta.ts` — an encoder and a decoder, both readable in
  full, no dependency). Measured on Lyra: an in-place change (a keystroke
  filtering a list, one row's badge flipping) falls to **1–4%** of the frame; a
  navigation, where the tree is genuinely different, lands near **80%** and is
  therefore left whole.

The choice of algorithm is the interesting part, because the obvious one is
wrong. A **structural** diff — walk both trees, emit `[path, value]` for every
leaf that moved — is what a virtual DOM does and what most people reach for. On
a navigation it measured **98–114%** of the frame it was replacing: worse than
useless, because every path string is overhead the frame itself doesn't carry,
and an unkeyed list reorder costs the whole list. A **byte** matcher has no
such cliff: it finds shared vocabulary between two trees that share no
structure at all, and it is indifferent to what the tree means. It is also far
simpler to prove correct, which for a layer that can silently desync a screen
is the dominant consideration.

Three rules keep it honest:

1. **One baseline per shell.** Every connection is level with the host's
   `sent` map — attaching serves whole frames, and a change fans out to
   everyone — so one encode serves the whole room.
2. **Decisively smaller, or whole.** A delta is only sent at ≤60% of the frame.
   Below that it wins even after deflate; above it, deflate on the whole frame
   does as well and the terminal does less work. And a frame over 64 KB is not
   compared at all: the encode's cost follows the size of the frames, not of
   the change, on the thread every session shares — about 7 ms at 64 KB, a
   second at 2.7 MB — so past that length, on either side of the change, the
   canvas is sent whole before anything is spent on finding out.
3. **Checksummed, always.** Every delta carries a hash of the frame it should
   rebuild. A terminal that lands anywhere else discards the result and sends
   `resync` — the failure this layer must not have is a screen that is silently
   wrong, and no saving is worth risking it.

Neither reduction is visible to an application, and neither changes a rendered
tree. A terminal that never advertises `delta=1` is served exactly what it was
served before either existed.

Two design points worth naming:

- **Transport → shell addressing.** The wire tags every event with its canvas;
  moss stamps the canvas's *active instance* as the event's origin, and nova's
  own origin filter delivers it to that instance's triggers alone. The terminal
  never learns what an origin is — addressing translation is the host's job,
  where both vocabularies meet. This is why two instances of one action on
  different canvases don't cross-fire, with zero client-side logic.
- **Identity is asked twice.** The HTTP surfaces re-ask the `session` verifier
  on every request; the socket asks at upgrade and then on a timer for as long
  as the connection lives. A verifier is the whole of what an app writes to
  give tokens a lifetime (`null` = expired), and moss never learns what expiry
  means — it only asks again. The socket half is not optional polish: asking
  once left an expired credential rendering happily while every endpoint the
  server shell called came back 401, because that wire rides the HTTP
  middleware. A live interface whose every load silently fails is worse than a
  lock screen. A verifier that *throws* is a fault, not a sign-out — a database
  blip must not become an outage, so only an explicit `null` closes.
- **Session lifecycle is capabilities, not channels.** The in-process function
  seam hands each session a `grant(token)` and a `revoke()`. A login function
  grants (the token goes down every connection as a `session` message; the
  terminals reconnect authenticated); a sign-out function revokes (every
  connection closes `4403`, the durable shell is evicted). The app calls
  capabilities; it never knows where the session machinery lives. On the wire,
  `4401` (stale token) and `4403` (sign-out) are the same recovery, not a
  retry: drop the token, reconnect anonymous — retrying with a stale token
  would loop forever.

## Server shells

One durable nova `Shell` per authenticated principal — the socket is ephemeral,
the shell survives disconnect (reattach re-sends the current trees, and N
attached connections receive the same frames: shared canvases are the same
mechanism). Anonymous connections get a throwaway shell, disposed on detach,
and so does every connection of a page.

**Every living shell follows a change.** When what the app consists of
changes, each shell that lives is brought level with what its principal holds
now, in place: the kept ones, a visitor's, and a page's, in every host. A
host holds every shell it built from when its build lands until it ends, and
lets it go where it ends, so the walk is over the shells that live and no
other. A role change (`invalidateIdentity`) rebuilds every living shell of
that principal the same way: the kept one, and each one a page built for them.

**Lifetime.** A durable shell lives until sign-out, an operator reset, or the
idle sweep: nothing attached for `runtime.shellIdleMs` (default 30 minutes) and
it is disposed, rebuilt on the next connection. Only the idle clock is swept —
deliberately no absolute cap, because that would discard the state of somebody
who is working. There is no TTL on the session behind it; a shell going away is
not a person being signed out.

**Enumeration and reset.** `shells.list()` reports every living shell the
host keeps — principal, attached connections, age, idle time, and what is
mounted on each canvas. `shells.reset(principal)` disposes one and stands its
replacement in the same slot, carrying the attached connections across, so every terminal
receives a frame and current trees exactly as on reattach. Both exist because
the host owns the map: an app that keeps its own note beside it can only drift
from it, and a shell nobody can enumerate is one nobody can be helped out of.
Since everything a shell derives from is re-read at build, the replacement is
the screen boot would serve that principal now — which is also the whole cost:
a reset is not a repair that preserves what someone was doing.

**One canvas cannot take the session down.** Rendering is guarded per canvas.
A tree that throws holds its last good frame and its neighbours keep rendering,
rather than killing the flush pass, escaping a microtask uncaught, and wedging
the shell for the life of the process.

The shell is built from the manifest: canvases and fragments are data; a canvas's
`initial` may be a candidate list, and the first action the principal holds
mounts — so login is the anonymous principal's application (the charter's
`public` grant) by derivation, not a special case. The role name is fixed and
the charter has to define it, as `public: []` if it grants nothing: boot does
not check that it is there, and a request that resolves to a missing `public`
fails. `inputs(session)` and
`seeds(session)` are the app's per-principal boot hooks — boot data and boot
instances. Endpoint calls ride the server's own HTTP surfaces
with the session's token — the server shell is just another principal-bound
client, enforcement included.

---

### A read that keeps answering

A shell's vex reads go through the session's wire — an in-process request
against this server's own mounts. For an entry declaring
`refresh: 'reactive'`, vex can keep answering after it returned: every
committed write to a table the query reads, it refetches (once for everybody
who would get the same rows) and calls back with the caller's new answer if it
changed (vex DESIGN.md, "Reactive reads").

The wire is the whole seam (`src/data/writes.ts`). Toward vex it is a `VexLive` — a
signal and a callback, carried in the in-process request's hono env, where no
network request can put one. Toward nova it is the response's `onChange`, which
nova applies through the endpoint's own `response` and `target`; the shell
re-renders and the change reaches every attached terminal like any data change.
No app code, no channel, no reaction.

The follow ends when the call's signal aborts (nova aborts it on unmount),
when the last subscriber leaves (nova unsubscribes when a newer call to the
same endpoint lands), or at once when nobody takes the response up — a server
function reading through the same wire follows nothing. An answer that changed
in the gap between the response and nova's subscription is held and handed
over on subscribe.

This retires most of "writers announce, viewers react" for data: a screen whose
reads are reactive needs no channel to stay current. Channels stay for signals
that are not data — a deck moving to its next slide mounts an action, which no
data update can do. Not built yet: carrying invalidations between processes
(the fabric would gain an `invalidate-tables` signal; until then each process's
rows TTL bounds it) and pausing the follows of a shell with nothing attached.

## The document

A page request is the one moment a terminal has nothing: the socket is not up,
the script may not have run. Moss can answer it with the screen itself, because
the screen is a value. `attach` already sends a frame and a tree per canvas;
`shells.snapshot` returns the same thing without attaching anybody, a terminal
that draws to a string turns it into markup with the app's own kit, and the
app's own `index.html` goes out with the screen in it and the snapshot beside
it. The browser's terminal starts from that snapshot and adopts the elements
already there. **A drawn page is a first frame delivered early**, and nothing
else: the socket still attaches, still sends current trees, and is still the
authority. If the two disagree — the shell moved in between, the cookie and the
stored token are different people — the next frame simply repaints.

What it costs was measured before it was built (atrium, one core, warm):
drawing a signed-in screen to a string is 0.4–0.7 ms; a whole anonymous document
— build a shell, read it, draw it — about 0.65 ms, some 1,500 a second. The
page carries its screen twice (markup and snapshot), 3–5 KB compressed.

**The head of the page is part of the screen.** A title, a description and a
preview card are facts about what the screen shows, so the page's `<head>` is a
node in a layout (nova's `nova:head`, holding the elements a head holds) and
rides in the same trees as everything else: `renderDocument` reads the
elements off the snapshot and writes them into the template's `<head>`, and the
terminal reads them off the wire to keep the page's head. Nothing was added to
the snapshot, the wire or the manifest for it. Where the page lives is known to
the writer without being told: the `site` plus the path the document was drawn
at.

Six things had to be true, and each is a small, separate decision:

- **The read does not disturb anybody.** A snapshot renders, and rendering can
  pick up a change the flush pass has not sent. `attach` handles that by making
  the newcomer's frame the whole room's; a snapshot handles it by not touching
  the `sent` baseline at all, so the next flush still finds its change.
- **It waits for the screen to be whole, but not for long.** `seeds` land a tick
  after the build, and mounts chain loads. Nova marks an instance `active` only
  after its mount hook has been awaited, so "the seeds are in and nothing is
  `initializing`" is the shell saying its first screen is complete — a signal
  that was always there. The wait is bounded (300 ms by default): a document
  must not hang on a slow integration, and a skeleton is a legitimate screen.
- **Who is asking comes off the browser's session cookie**, because a page
  request carries nothing else (*Where a browser keeps its session*). It is
  read here and never written: the upgrade is where it is set and taken back,
  and one writer cannot disagree with itself. A cross-site request that carried
  it could draw a page it cannot read.
- **An anonymous page and its socket are two shells that must name their
  instances alike.** A principal's page is drawn from their durable shell and
  the socket attaches to that same shell — same ids, nothing to arrange. Nobody
  has a durable shell: the page is drawn from one throwaway shell and the socket
  builds another. So the first is built under a random *seed* and mints its ids
  in order under it; the terminal names the seed on the upgrade; the second
  mints the same ids. Plain counters would have been simpler and wrong — the
  origin an event claims is checked against the canvas it names, and that check
  is only worth anything while a stale id from another shell names nothing.
- **Who may keep a page follows from who asked.** Drawn for nobody, a page is
  the same for everybody and may be cached or written to a file; drawn for
  somebody, it is theirs (`private, no-store`). That is one function
  (in `src/documents/document.ts`; `renderDocument` answers what it says as
  the page's `headers`), keyed on whether a principal resolved. No page, route or
  app declares which kind it is, so none can declare it wrong.
- **Nothing here can fail a page.** A kit component that throws, a shell that
  will not build, a template without a root: the template goes out as it is and
  the terminal paints it the way it always did.

The route is the app's, and so are the template and the kit. Moss draws nothing
itself: `terminal/react/server`, `terminal/vue/server` and `terminal/dom/server`
are terminals like the others — handed a screen, they draw it — and the same
shape as lyceum's SSH door, which has drawn served trees inside a server
process since before this existed.

One thing a kit must hold up its end of: its first pass in the browser has to
draw what the server drew. Anything that differs between the two — the window's
width, a portal target, storage — may not decide a component's first render.
Lyra's kit answered "is the window wide" from `window.matchMedia` in a state
initialiser, behind a `typeof window` guard; the guard is the bug, because it
makes the two sides disagree about the same render, and on a wide screen React
threw the page's elements away. What a component writes onto `<html>` from an
effect (a palette) is not in the page until the script runs, so the app reads it
off the snapshot and hands it to the document. Each app holds both with a check.

## Pages

`shell` is the app: one durable shell per person, the thing that rearranges
itself around whoever holds it. Not everything at a path is that. A welcome, the
docs, a published talk are read, not worked in, and a person who is signed in is
still the same person when they open one.

A **page** is a shell manifest and the path it is drawn at
(`app.pages`). It is drawn by the same machinery with the keeping taken out: a
shell is built from the page's manifest for whoever asked, read, and disposed.
Everything else is unchanged, which is the point —

- what exists on it is still ring 1. A strip that names the signed-in person is
  an action a member is granted, on a canvas whose candidates a stranger is
  granted none of. Being signed in changes what a page *has*, through the
  charter, and nothing in a layout asks;
- its reads still run under the caller's policy;
- its cacheability is still decided by who asked, in the one place above.

So a signed-in person on `/docs` is themselves — and still has no shell there.
Their app shell is neither built nor touched.

**Does anything stand behind a page afterwards?** Only if something on it can
still happen. Every reason a drawn screen is not finished is a declared part of
an action — a `ui:` trigger, a two-way binding, a channel it waits on, a read
that keeps answering — so the question is decidable from the definitions
actually mounted, without running anything. Nova reports those facts
(`livenessOf`); moss, which holds the entries behind each fingerprint, composes
the verdict (`src/shells/snapshot.ts`) and puts it on the snapshot as `live`. A page that
is not live is served as markup and its terminal opens no socket at all. A page
that is live gets a shell for as long as its connection lasts — per connection,
never per person. The one thing counted live without being read is code: a `fn:`
endpoint that something calls.

`onSession` does not run for a page. That is not an economy: session code is
arbitrary, and a host that ran it could no longer say what a page can do. It is
the app's, and the app's shell is always live.

**Pages as files.** `exportDocuments` is the same draw, for nobody, once per
path. It takes no credential and has no way to be given one, so a file is only
ever the page as nobody in particular sees it. Each file reports whether it is
live and which reads its markup was drawn with (frozen at the moment of
writing). A live page is still written — whether a server stands beside the
files is the build's to know.

**What a path means, and what it does not.** A path picks between the app and a
page, and can seed a page's canvas with its parameters. It does not address a
place *inside* the app: the app's shell is state, one per person, shared by
every terminal they have open, and a URL that tried to name a position in it
would be naming something two tabs cannot disagree about. Routing stays where
the application guide puts it — shell state is the truth.

## The function seam

The `fn:` escape hatch runs server-side, in-process, next to the durable shell.
The manifest's `functions(session)` builds handlers once per session, closing
over the living shell, who is calling (the principal, their roles, what they
hold), the server's own surfaces as that session (`wire`), and `grant`/`revoke`.
A session says who is calling and holds no key: it carries no database pool, no
operator key and no policy object, and what app code needs of those has a call
(DOCS.md, `FunctionSession`). Agents live here (their reads run under the
caller's policy — they see what the caller sees); keys are `.env`. This is deliberately the *side-
effect-ful, session-bound* half. The canonical, side-effect-free function — an
independently deployable endpoint under `/fns` — is specified and pending.

## The write lanes

Vex is the choke point every application write passes through, which makes its
write observer the one place a host can learn about all of them. Moss
hears it once and offers the news two ways — and the split is a privilege
boundary, not a convenience.

**The app lane carries no rows.** A declared reaction hears table, op, row
*count* and the unforgeable scope. That is enough to invalidate, resync, or
push a ping onto a living shell, and it is not enough to leak anything: a
receiver that wants data re-reads it under its own policy, through the same
governed wire as everybody else. The alternative — handing rows to a callback
— puts data in the one place this stack has no fence for, arbitrary imperative
code, and `deliver` payloads are pings for exactly the same reason.

**The tide lane carries rows, because tide has the fence.** It is a feature
(`createTideFacts`, `@niscorp/moss/tide`), not part of the core: it hears a
write through the one seam a feature is handed for that, and an app that runs
no tide names none. A fact is stamped
with the identity derived from the *write's own* scope, and the matcher offers
a fact only to reflexes running under that same identity. Tide is the one
place a row travels without being read — no policy is consulted on the way —
so that stamp is the whole boundary, and it is the app's naming rule, never a
listener's interest, that sets it. Stamping by who is listening would walk one
tenant's row into another tenant's effect, which is a defect this stack has
already had once and does not intend to have again.

Interest is **declared, not discovered**: reactions name their table up front
and moss routes by that name, so app code never string-matches a fingerprint
to work out what happened. The cost of the declaration is that a name nothing
writes fires nothing, quietly — derive the names from `mutationEffect` and
assert them, the way an app already asserts its charter.

**Pacing belongs to the host, not the engine.** Tide names the next instant it
wants waking for; the driver owns the timer, wakes on every ingest, and drains
to quiescence. The driver is tide's own (`createTideDriver`), as the Postgres
store is (`@niscorp/tide/postgres`): both were moss's, and they are tide's
pacing and tide's storage. The interval that used to stand in for this was both a latency
floor and a throughput ceiling — a chain advanced one hop per beat — and the
only periodic thing left is a janitor that finds nothing when nothing is
broken.

## Model runs

An agent lives behind the function seam, so the app server is the one place that
sees every model call a session makes. The manifest declares a `runs` sink;
`session.recordRun` stamps `at`, `principal` and `shellId` and hands over a
`RunRecord` — which agent, which model, what it cost, and the whole exchange as
`RunTurn[]`: every message out, every tool call with its arguments, every result
back.

`RunTurn` is deliberately moss's own shape. Moss depends on neither an LLM client
nor an agent framework, and a client's message union leaking into the app server
would make one provider's idea of a turn the contract. The caller flattens
whatever it ran into this, once, at the seam that knows both sides. Arguments stay
a string because providers send them JSON-encoded, and re-encoding a parse is how
a malformed call stops looking malformed in the record.

Nothing is aggregated, and there is no cost in currency: per-model price tables
rot, tokens are the honest unit, and `provider` + `model` let pricing be layered on
later. Per person, per model, per agent and per day are group-bys over the rows.

**No default sink.** A bounded in-memory array would answer "the last few minutes,
gone on restart", which is not a question anyone asks about an agent's behaviour.
Unset means unrecorded, which is at least honest. The session rides along so a sink
can persist through the CALLER's own governed wire rather than a privileged one —
a record that could be written on another principal's behalf would be worth
nothing.

## What it refuses to be

| Temptation | Refusal |
|---|---|
| An auth provider | BYO identity. Who somebody is, the app decides at its own door (magic link is the default strategy); the server only asks who a token is. Moss does keep a credential an app may use for that: `mintSession` stores a token by its hash once the app has decided, and `'dev-open'` trusts an unsigned one for a harness. Neither decides anything. |
| A framework | An assembly point. The intelligence lives in the artifacts; the server resolves, serves, pushes, enforces. Its core is eleven modules, each one question or the wiring, and a twelfth that runs it on Node (*How the code is laid out*). |
| A place where policy hides | Every opinion it enforces is a readable artifact: charter, scope policy, assertion. No annotations, no interceptor magic. |
| A second way to do anything | Reads are vex, writes are vex, compute is functions, UI is actions, permissions are the charter. Moss's core adds no vocabulary of its own, and holds itself to the same: the rows it keeps it reads and writes through vex entries, and the statements left outside vex are counted, each with its reason (`pnpm check:raw-sql`). |
| A web framework | A page is a shell manifest at a path; a drawn document is a first frame delivered early. No file-system routes, no layouts-of-layouts, no loaders, and no chain of middleware for an app to configure (one gate may be laid over the operator prefix, `runtime.operatorGate`). A page's `<head>` is not managed either: it is a node in a layout, written into the document like the rest of the screen. The template, the route and the kit are the app's. |

## Deliberately unbuilt

On the record, so nothing reads as finished that isn't:

- **The artifact library.** Versioned artifact rows + snapshots + a published
  pointer per channel, so publish is a runtime write, not a rebuild — the thing
  §2 ("deployment is a data operation") is building toward. The open decisions
  are tenancy overlays, draft/published channels, and live-shell migration on
  publish (nova needs a `replaceAction` that rebinds live instances without
  dropping their data). Verify-at-boot becomes verify-at-publish, same gates.
- **The `/fns` host** — named, side-effect-free, independently deployable
  functions under `/fns`, gated by catalog closure.
- **The projection model** — per-identity state held in the DB, written-and-
  pushed, seeded on login with no AI call, held while the principal is offline.
- **The working set (level streaming).** For large apps and AI context budgets,
  the catalog itself streams: serve the graph neighborhood of a principal's
  active actions now, fault the rest in on demand. Level streaming for humans is
  context management for models — one mechanism, two products.
- **Scale-out** beyond sticky sessions pinned by session id. What crosses
  processes today is invalidation only: `refresh` moves a generation pointer
  every process polls (`src/artifactory/generation.ts` holds the clock; the row is
  read and moved through vex, `src/data/own-entries.ts`), and an optional fabric
  (`runtime.fabric`, `src/server/signals.ts`) carries identity invalidations, nudges, and the word to adopt for one principal.
  A set of actions does not cross: a process that follows the pointer reads
  again the sets it has a loader for, and a set handed to another process
  with `replaceActions` alone never reaches it (DOCS.md, "Many processes").
  A shell is still resident in one process. A drawn document
  adds one requirement to stickiness: the page request and the socket should
  land on the same process, or a principal's shell is built twice and the page
  repaints once. And one limit is each process's own: the integrations'
  webhook door lets 240 calls a minute through for each integration, counted
  in the memory of the process that takes them, so N processes let a vendor
  through up to N times as often. One ceiling for a deployment would be a
  store its processes share, with a write for every call; it is not built,
  and a deployment that needs it sets it at its proxy.
- **Opening the app at a position.** A path that seeds the *app's* shell (a
  deep link to a record) is not built. It is not the same thing as a page's
  parameters: the app's shell is durable and shared across a person's
  terminals, so an opener has to be idempotent against whatever is already
  mounted, and decide what a reload means after they have moved on.
- **Letting a page's connection go.** A terminal that holds a token on a page
  drawn for nobody (a file, on a static host) connects, and is served a page
  shell for as long as it stays — even when that page turns out not to be live
  for them either. Closing such a connection once its frames are sent is unbuilt.
- **A seal no script can supply, in every browser.** The seal is given by the
  answer to a browser's first upgrade, after the page's script has started; a
  script already running then could set one of its own first, and open the
  sign-in that follows. Its `__Http-` name is what stops that — a browser lets
  only an HTTP answer set it — and a browser that does not know the prefix
  does not stop it. Nor does the name stop a *server* on a sibling subdomain
  from setting one; with a script in the page as well, that is the same
  opening. `__Host-Http-` would close it, and would put the seal on every page
  request, ending the sharing of pages drawn for nobody. Over plain http there
  is no such name, and nothing on the way is private either.

## Boundaries

Moss composes the other nisc packages and owns none of their concerns: charter
resolves and verifies, vex reads and writes under scope, nova is the UI (headless
core + a framework adapter), prism is the endpoint transform language. Moss is
the organ that was missing — the trusted place where the charter executes, the
shell runs, and existence is served. See [DOCS.md](DOCS.md) for the API.
