Skip to content

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.

The unit of the substrate is the frame: a payload published under a string key, stamped with a version and a composition time.

the wire shape
{ 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:

The snapshot pipeline. A brass producer box, composeBundle or workflow run output, flows through a dashed fail-closed validate gate into the snapshot manager's cache, then over WebSocket to the browser canvas. A dashed edge below shows late joiners fetching the latest frame and then subscribing.

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.

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:

  1. 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.
  2. 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”.
  3. 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.

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.

KindRenders as
markdownFormatted text.
viewA typed visualization: a table, a graph of nodes and edges, or a board.
htmlUntrusted 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.

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:status as 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.

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.

  • 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 SnapshotManager interface and the board section vocabulary.