# Statum Model ## Statum Frames A statum frame consists of a JSON object with three keys: - `content`: a string containing the literal content of the frame. - `signer`: a string containing a signer ID. - `signature`: a base64 encoded signature verifying the content authored by the signer. ## Statum Slot - `key`: a server issued ID for the slot. - `scope`: a string value that can be one of: - `"device"`: persists between browser sessions. Backed by `localStorage`. - `"session"`: persists within the browser session (e.g. between tabs). Backed by `localStorage`, kept consistent across tabs via `BroadcastChannel`, and cleared on browser close via a session-cookie marker (see [Initialization](#initialization)). - `"flow"`: persists only within the current tab, while the tab remains on the site (e.g. persists between navigations). Backed by `sessionStorage` (per-tab, survives navigation, cleared when the tab closes). - `"page"`: persists only while the current page is loaded (e.g. clears on navigate). Held in memory. - `"transient"`: does not persist, will never get sent back to the server - random or `null` key each time. Held in memory. - `type`: an application defined type name. ## Statum Snapshot A statum snapshot captures an aspect of the application state at a particular point-in-time. It is usually wrapped in a frame, which must be retained if it is to be sent back to the server after the `transmit_after` time. It contains: - `as_at`: an ISO 8601 string representing the time the snapshot was authored. - `slot`: a slot object (above) - `transmit_after`: an ISO 8601 string representing the time the slot will no longer be cached by the server and the application will need to send the snapshot. `undefined` if this is a unidirectional snapshot that never needs to be sent to the server. If it equals `as_at`, then the snapshot must always be sent. - `invalid_after`: an ISO 8601 string representing the time this snapshot should be considered invalid and not used. - `private`: a base64 encoded string of data only readable by the application server. - `public`: a JSON object with application-defined key/values. This is the user-facing data that the JS and HTML APIs bind to. It may also contain embedded [Statum Actions](#statum-action), referenced by path (e.g. `typeName.deleteItem`). ## Statum Action - `uri`: The URI to request. - `method`: The HTTP verb to use. - `private`: A base64 encoded string of data only readable by the application server (can be `undefined`) ## Statum Directive - `type`: One of - `"navigate"`: Used to redirect the frontend to a new URI. - `"post"`: Used to perform a full-page form POST navigation to a URI. A `"post"` directive is always executed after all other directives in the same response. - `"set"`: Used to set a slot to a new snapshot. - `"clear"`: Used to clear a slot. - `"error"`: Used to report an error to the frontend. - `"subscribe"`: Used to subscribe the client's realtime channel to a slot. - `"unsubscribe"`: Used to unsubscribe the client's realtime channel from a slot. - `"notify"`: Used to surface a transient notification to the user. Depending on the type, it will have other properties. ### Navigate Directive - `uri`: The URI to navigate to. ### Post Directive - `uri`: The URI to perform a form post to. - `data`: Key/value pairs for form data. The post directive performs a full-page browser navigation via a real form POST, not an AJAX request, so its response is HTML and is not parsed for directives. Because it navigates away from the page, a post directive is always executed after every other directive in the same response has been applied. ### Set Directive - `snapshot_frame`: A frame containing a snapshot. Set directives are idempotent. Each tab keeps a small in-memory map of slot key → frame `signature` and skips the store update, persistence, listener notification, and re-render when an incoming set carries a signature already recorded for that slot. The `signature` is a signed cryptographic hash of the frame's `content`, so a collision across genuinely different content is negligible. The map lives in memory only (not the persistent backend), so a frame another tab has already written to storage still triggers a re-draw in a tab whose in-memory tracking lags behind; and an entry is dropped whenever its slot is cleared, so a replayed frame re-applies after a clear. ### Clear Directive - `key`: The key of the slot to clear. ### Error Directive - `message`: A human-readable error message (optional). - `code`: An application-defined error code (optional). - `data`: Additional key/value error details (optional). An error directive signals a failure to the frontend. When one is received, the `statum.act` promise for the originating action rejects and any `stm-on-error` handling on the originating element runs. An error directive returned from the entrypoint load is surfaced as a load failure. ### Subscribe Directive - `key`: The key of the slot to subscribe the client's realtime channel to. Tells the client to begin receiving live updates for a slot over its realtime channel (see [Channel Endpoint](#channel-endpoint)). Subscribe is not terminal: it runs in the same pass as `set` and `clear`, and the channel operations it triggers are asynchronous and do not block the remaining directives in the response. ### Unsubscribe Directive - `key`: The key of the slot to unsubscribe the client's realtime channel from. Removes a slot from the client's realtime channel subscriptions. Subscriptions are reference-counted across tabs: a slot is only removed from the server when the last subscription to it across all tabs is dropped, and the channel connection is closed when the final subscription of any kind is removed. ### Notify Directive - `kind`: An application-defined notification category string (e.g. `"info"`, `"warning"`, `"success"`). It is opaque to the library, passed through to the handler, and does not affect the default behaviour. (The category is named `kind` rather than `type` because `type` is already the directive discriminator.) - `message`: A human-readable message string. Surfaces a transient message to the user. The default handler calls the browser's `alert()` with the `message`; this can be customised via `statum.onNotifyHandler` (see [Handlers](#handlers)). Notify runs in the same pass as `set` and `clear`. # Statum HTTP API All endpoints that return directives do so as a JSON array of Statum Directives, served with the Content-Type `application/vnd.statum+json`. The frontend uses this content type to distinguish a Statum directive response from an ordinary HTML or JSON response. ## Entrypoint Endpoint `GET /_statum/entrypoint`: Get initial directives based on the URL. Has query param `uri` set to the browser's current `window.location`. Returns a JSON array of Statum Directives. ## Slot Post Endpoint `POST /_statum/slots`: Send a saved state snapshots frame to the server once it has reached its `transmit_after` time window. Sent as a JSON array of Statum Frames. Returns a JSON array of Statum Directives. ## Action Endpoints The URLs of action endpoints are entirely defined by the action itself (in the `uri` property). An additional `X-Statum-Private` header should contain the `private` data of the action if present. Returns a JSON array of Statum Directives. ## Channel Endpoint The channel endpoint is the realtime delivery mechanism for live slot updates. It is a Server-Sent Events stream multiplexed across the slots the client has subscribed to. `GET /_statum/channel`: Opens the SSE stream. The first event carries the channel ID: - `event: channel` — `data` is the channel ID string. Subsequent events deliver slot updates, one directive per event: - `event: set` — `data` is a JSON snapshot frame (the same `{content, signer, signature}` object used by a set directive's `snapshot_frame`). - `event: clear` — `data` is the slot key of the cleared slot. Only `set` and `clear` events are delivered over the channel. The client opens a single connection — shared across tabs via a `SharedWorker`, or one per tab when `SharedWorker` is unavailable — on the first subscribe directive, and closes it once it has no subscriptions left. `PATCH /_statum/channel/{id}`: Adds or removes the channel's subscriptions. The `{id}` path segment is the channel ID delivered by the stream's first event. The request body is a JSON object: - `subscribe`: an array of slot keys to begin receiving updates for. - `unsubscribe`: an array of slot keys to stop receiving updates for. Either array may be omitted or empty; the client coalesces multiple subscribe/unsubscribe directives into a single PATCH where possible. The response body is not used. ## Request Headers All HTTP API requests must carry an `X-Statum-Slot` header for each slot the client currently holds (i.e. every snapshot in scope for the current page/flow). Note that sending base64 frames in headers can approach practical header-size limits on some servers and proxies; very large frames are better refreshed via the slot post endpoint flow below. There are two variations of this header: - For slots whose snapshot is still within the `transmit_after` window, an `as_at` timestamp reference is sent, e.g. `X-Statum-Slot: key=d3cceb88-c211-41ee-a0b1-6a93f3b6331b; as_at=2026-07-06T07:33:02.332Z`. - For slots whose `transmit_after` equals its `as_at` timestamp (always-send), the frame is sent in base64, e.g. `X-Statum-Slot: eyAiY29udGVudCI6ICIuLi4uIiwgInNpZ25lciI6ICJzMSIsICJzaWduYXR1cmUiOiAiZXlBaVkyOXVkR1Z1ZENJNklDSXVMaTR1SWl3Z0luTnBaMjVsY2lJNklDSnpNU0lzSUNKemFXZHVZWFIxY21VaU9pQWlJbjA9In0=` ### Snapshot freshness decision For each held slot, the client compares the current time against the snapshot's timestamps: - `now < transmit_after` (and `transmit_after > as_at`): send the `as_at` reference header. - `transmit_after == as_at` (always-send): send the base64 frame header. - `transmit_after < now < invalid_after`: the server's cache for this `as_at` has expired. Before sending the request, issue a pre-flight request to the slot post endpoint; it returns `Set` directives (refreshed snapshots with new `as_at` values), after which the request proceeds with reference headers. - `now >= invalid_after`: the snapshot is no longer usable. Drop it locally, do not send a header for it, and refetch the slot via the entrypoint or an action response. Thus a pre-flight is made only when one or more held snapshots have passed their `transmit_after` time but have not yet reached their `invalid_after` time. # Javascript API The Javascript API provides access to the state and actions via the global `statum` object. It automatically handles pre-flight requests, headers, and executing directives from the server. Note that the Javascript library will also automatically make a request to the Statum Entrypoint URL on page load to get the initial states in order to fill out the page template. State is exposed to the application keyed by **slot type**. Statum assumes at most one slot per type, so each type name maps to a single current snapshot; the slot's `key` is used only on the wire (in headers and directives). ## Initialization On startup, before fetching the entrypoint or evaluating any bindings, the library checks for a `_statum_sdc` session cookie: 1. If the cookie is absent, this is a fresh browser session: clear all `"session"`-scoped slots from `localStorage`, then set `_statum_sdc=1` (a session cookie, so the browser drops it on close). 2. If the cookie is present, the browser session is already underway; do nothing. Because session cookies are shared across all tabs of a browser session, a tab opened mid-session sees the marker the first tab set and does not wipe session data. This step must run before anything else so stale session state is never read or sent. Caveats: browsers with a "continue/restore session" feature preserve session cookies across restarts, so in that mode stale session data may not be cleared on reopen; and if cookies are blocked entirely, `"session"` scope degrades to per-page-load. ## Managing State State snapshots can be cleaned up via `statum.clear("typeName")`. All snapshots can be cleared via `statum.clear()`. ## Reading State State can be read via `statum.read("typeName")` which returns the `public` data object currently associated with the type "typeName". This is the same value the HTML attributes bind to. To get the full snapshot object (including `as_at`, `slot`, `transmit_after`, etc.), use `statum.getSnapshot("typeName")`. To get the frame that wraps a snapshot, use `statum.getFrame("typeName")`. A full state object can be generated via `statum.state()` which returns a javascript object containing properties named after each type name that currently has a state, their values being that type's `public` data, e.g. `{ "typeName": { "label": "..." } }`. ## Performing an Action To perform an action, use `statum.act(action)` where `action` is a Statum action. Use `statum.act(action, data)` to send parameters along with the request. This is encoded as query params when `action.method` == `GET`, or encoded as form encoded otherwise. `statum.act` returns a promise which resolves when the request has completed and all directives have been run. The act function can also be used with a string referring to an action name ## Reacting to State Callbacks can be attached to statum using `statum.addStateListener("typeName", callback)` which calls `callback` with an object containing: - `type`: the type name - `frame`: the frame (can be `null` if state has been cleared) - `snapshot`: the snapshot object (can be `null` if state has been cleared). It is fired whenever a state of the specified type is set or cleared. A listener can be removed with `statum.removeStateListener("typeName", callback)`. For `"session"` and `"device"` scoped slots, state changes are propagated to other tabs in the same origin via a `BroadcastChannel`, so listeners fire in every tab, not just the one that performed the change. ## Handlers The default UI behaviours for confirmations, notifications, and errors can be customised by assigning handler functions on the `statum` object. - `statum.onConfirmHandler`: `(message, element) => boolean | Promise`. Invoked for an action element carrying `stm-confirm` before the action runs; the action proceeds only if the handler resolves truthy. Defaults to `window.confirm(message)`. - `statum.onNotifyHandler`: `(kind, message) => void`. Invoked for each `notify` directive. Defaults to `window.alert(message)`. - `statum.onErrorHandler`: `(error, element) => boolean | Promise`. Invoked for every `error` directive, entrypoint load failure, and HTTP/network failure. `element` is the originating action element, or `null` for the entrypoint. Returning (or resolving) `false` suppresses the default behaviour (rejecting the originating `statum.act` promise and applying any `stm-on-error` classes); any other return value leaves the default in place. ## Realtime channel Live slot updates are delivered over the realtime channel (see [Channel Endpoint](#channel-endpoint)). The library manages this automatically in response to `subscribe` and `unsubscribe` directives: it opens a shared connection via a `SharedWorker` (script url overridable with `stm-worker`, default `/_statum/worker.js`) on the first subscribe, reference-counts subscriptions across tabs, and relays incoming `set`/`clear` events to every tab through the existing cross-tab `BroadcastChannel`. When `SharedWorker` is unavailable each tab opens its own connection and broadcasts the events it receives to its siblings. In every case a tab applies each distinct frame exactly once, thanks to [set-directive idempotency](#set-directive). # HTML API The Javascript library also has some functionality inspired by HTMX using attributes prefixed with `stm-`. Usually their values refer to a value reached by following a path into a slot's `public` data, optionally combined with a loop variable (see [Loops](#loops)). The template is re-evaluated each time the state changes. ### Expressions Attribute values used as predicates (`stm-if`, `stm-else-if`, `stm-class.x`) are evaluated as JavaScript expressions against a scope built from the current state. Because the page's HTML is trusted, expressions are evaluated directly rather than via a restricted parser. In this scope each type name is bound to that slot's current `public` object, plus any in-scope loop variables. For example, `stm-if="typeName.level > 5"` evaluates `typeName.level > 5` with `typeName` bound to that type's `public` data. ## URL Overrides The attribute `stm-entrypoint` can be used *only* on the `` tag, to override the default entrypoint url of `/_statum/entrypoint`. Same goes for the `stm-slots` attribute (default slot post url `/_statum/slots`), the `stm-channel` attribute (default realtime channel url `/_statum/channel`), and the `stm-worker` attribute (default `SharedWorker` script url `/_statum/worker.js`). ## Boot and Loading States The library fetches the entrypoint on load before the template is fully usable. Two attributes control element visibility around this: - `stm-preloader`: the element is shown until the entrypoint response has been processed, then hidden. - `stm-content`: the element is hidden until the entrypoint response has been processed, then shown. These allow a page to display a loading state while state is fetched, then reveal the bound content once it is available. To prevent a flash of the un-bound template before the script runs, authors may pre-set the `hidden` attribute directly on `stm-content` in the HTML (e.g. `