Snapshots and surfaces
A rib has no way to ship its own UI, by design: it publishes data, and the harness draws it. The rib hands over typed payloads under namespaced keys, and the browser renders every rib with the same small set of components, one view that serves all of them. The alternative is the trap most extension systems fall into, where either extensions embed their own web views and the host becomes a browser for mutually distrustful apps, or they get no UI and live as command-line afterthoughts.
The substrate underneath has four moving parts: frames, the snapshot manager, the canvas vocabulary, and the surface layout model.
Frames on keys
Section titled “Frames on keys”The unit of the substrate is the frame: a payload published under a string key, stamped with a version and a composition time.
{ type: "snapshot_update", key: "rib:example:board", version: 7, composedAt: "2026-06-09T18:20:04Z", data: ... }A producer registers a composer under a key, a function that returns the payload. Composition is lazy: the composer runs when something asks for a recompose, not on a timer the producer manages. Each successful compose increments the version, replaces the cached frame, and broadcasts to every subscriber. Concurrent recompose calls coalesce into one composer run, and versions let a late-joining client discard an out-of-order frame.
The figure traces the whole path from producer to pixels:
Figure 1. One pipeline for every producer. The validate gate runs before the cache, the WebSocket carries versioned frames, and a late joiner reads the cached frame before subscribing.
Fail closed, three times
Section titled “Fail closed, three times”The pipeline’s defining property is where it puts its checks. A frame can be rejected at three points, and every rejection protects the last good state:
- At the producer. A key can register a validator, canonically a zod
parse. An invalid payload is dropped before it is cached or broadcast, and the prior frame survives. - At the composer. If the compose function throws, the failure is logged once and the cached frame is untouched. A flaky external system degrades a board to “stale”, never to “broken”.
- At the renderer. The browser parses every frame against the canvas schema before rendering. A mismatch renders a one-line error note instead of a component, so a malformed payload cannot take the page down.
The combined effect is the substrate’s contract with the reader of a board: what you see is the last payload that passed validation, with its composition time shown, and nothing else ever reaches the screen.
The canvas vocabulary
Section titled “The canvas vocabulary”Renderers are a closed set, on purpose. A payload declares what it is, and the browser either has a renderer for that shape or refuses politely.
| Kind | Renders as |
|---|---|
markdown | Formatted text. |
view | A typed visualization: a table, a graph of nodes and edges, or a board. |
html | Untrusted rib-supplied markup rendered in a sandboxed iframe. The host owns the document shell (a CSP with connect-src 'none' blocks script-initiated network connections; a bridge script is injected first). Frame script calls keelson.action(type, payload) to post typed actions back; the rib’s handler receives them with origin: "canvas-html", which the host stamps itself after reading only type and payload off the frame message, so a frame can never claim "board". The markup is untrusted (rib- or LLM-authored, and frame script can post on load without a user gesture), so a rib’s onAction should gate frame-origin verbs to a safe subset. The shell stamps the app’s resolved theme as data-theme on the frame’s root element and pushes later toggles into the frame, so markup styled through :root[data-theme] token overrides re-themes live. |
HTML views can opt into preserving small UI drafts across document replacement
with keelson.saveState and keelson.onRestore. State is isolated by snapshot
key and retained only in bounded browser-tab memory, not durable rib storage.
The HTML state bridge reference
defines restoration timing, placement sharing, and limits.
The board is the workhorse: a dashboard composed from a fixed section
vocabulary, stats, segments, bars, tables, cards, rows, actions, a grid, a chart
(a deterministic line or timeseries plot), seats (a fixed-capacity identity row),
journey (a numbered next-steps strip), and graph (a ranked dependency map), with
one level of columns for layout. Prefer graph over an html region for node-link
maps: the host owns layout, theme, chain highlighting, the narrow fallback, and
trusted node actions. Sections carry tones, a small
closed palette: a semantic core (ok, warn, error, neutral), a few
decorative accents, and five reserved identity hues. The identity hues are not
decorative. A rib that renders the same actors repeatedly assigns one per actor
at creation and persists it, never hashing per render and never seating an actor
in a status hue, so a face keeps its color while status is free to change under
it. An actions section is one way a board gets buttons: each action posts back
to the rib that owns the board’s key, and the rib’s handler decides what it
means. Cards carry their own actions (a whole card body can be one), and a
region head carries a menu of rib verbs. All of them reach the same handler.
Views and surfaces
Section titled “Views and surfaces”Payloads need placement, and a rib declares it statically, as data, in the contract:
- A view binds one snapshot key to one canvas kind: “render
rib:example:statusas a board.” Views are the small unit, a single live panel. - A surface is a full tab in the browser: a layout of regions, each region bound to a snapshot key, arranged as an optional header, a banner, rows of columns, and a footer. The rib declares titles and glyphs; the harness owns every pixel of chrome.
The two are not independent. A region’s canvas kind is resolved from the rib’s
views entry for the same key, defaulting to view when no entry claims it, so
a key a rib declares as html renders as sandboxed markup inline on the surface
rather than only in the canvas drawer. One declaration governs both placements.
A surface’s own fields say how the tab presents itself. title names the nav
tab. heading is an opt-in H1, independent of the title, and a surface that
omits it renders no H1 at all. subtitle sits beneath the heading,
projectScoped opts the surface into the host’s shared project picker, and
hideRegionActions opts an authoring console out of the host’s per-region
chrome. Banner regions are the one constrained slot: a banner never collapses
and never hides.
A rib can also add regions at runtime via ctx.registerRegion(surfaceId, region) (where ctx is the RibContext passed to activate). The call appends the region to the named surface grouped by region.group and returns an unregister handle; the harness bumps the manifest-revision beacon so subscribed browsers re-fetch the layout without a page reload. surfaceId must name a surface the rib declared statically; region.key must be under rib:<id>:*. The method is optional on older harnesses. A rib should guard with ctx.registerRegion?.(...) so it degrades cleanly.
That call is the only way to add a region later, because views and surfaces are
live in different degrees. A rib may append to its views array at runtime and
call ctx.invalidateManifest?.(), since a view carries no boot-derived state and
a live one is a complete one. A region’s workflow name and cadenceMs are read
once at boot, when the harness walks every manifest to derive which workflows may
refresh which region and which regions the heartbeat owns. A region a rib pushes
onto its own descriptor after that walk would serve unbound and never get a
heartbeat, however live the manifest looks. serverRefresh is also read during
that boot-time derivation.
A region can also name a workflow: the catalog workflow whose run
repopulates the region’s key. That single field is what makes boards
operable. The refresh button on a region runs the workflow, and a cadenceMs
on the region (floored at thirty seconds) asks for the same run on a schedule.
The harness keeps that schedule itself. A single server heartbeat uses the
declared cadence while a tab subscribes to the region’s key. With no subscriber,
it backs off to six times that cadence instead of stopping, so an idle board
stays warm without refreshing at its viewed rate. The heartbeat is deliberately
cheap: a tick admits at most two new runs, and it reads staleness from each
frame’s composedAt, so an operator’s refresh and a server tick suppress each
other rather than double-run. KEELSON_DISABLE_SCHEDULER=1 turns it off.
Two cases stay client-driven. A region carrying workflowArgs refreshes only
while its surface is open, because the heartbeat would fire the run with empty
inputs and hand an args-expecting producer nothing. One boot summary warning
lists every such key. A region with serverRefresh: false also stays off the
heartbeat, silently because the rib declared that intent. Its client cadence
still applies while the surface is open.
Freshness stays visible either way. Every frame carries its composition time, so a board reports how old it is rather than implying it is live.
Beyond key, workflow, and cadenceMs, a region carries workflowArgs,
the server-only serverRefresh flag, and presentation fields the harness honors:
group and groupTitle, byline,
collapsible and collapsed, live, hideWhenEmpty, and headActions
(menu-only verbs on the region head). The
rib contract documents each one.
One substrate, no exceptions
Section titled “One substrate, no exceptions”The substrate is not a rib-only side channel. Workflow runs publish their
progress under run-scoped keys the browser watches the same way, and the
built-in surfaces ride the same pump as rib surfaces: the harness registers a
built-in USAGE_PULSE_SNAPSHOT_KEY stream that feeds the first-party Usage
surface exactly as a namespaced rib key feeds a rib surface. The strongest
evidence is the shape of the web app itself: its views directory holds Chat,
Workflows, Memory, Usage, and a single generic Surface component that renders
every rib ever installed. When a new rib lights up a new tab, no new UI code
ships; the rib only published data.
Where to go next
Section titled “Where to go next”- The rib model places snapshots among the other contributions a rib can make.
- Workflows covers the runs that repopulate region keys.
- Snapshots and the canvas is the precise
contract: the
SnapshotManagerinterface and the board section vocabulary.