# 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. - `"reenter"`: Used to re-fetch the page's entrypoint for the current URL and apply its directives in place — no page reload. Carries no other properties. - `"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. - `"transmit"`: Used to force the client to transmit a slot's frame to the server immediately, optionally retrying the originating request afterward. - `"invalidate"`: Tells the client to drop the held slot whose frame carries the given `signature`. Emitted automatically by the slot post endpoint for a frame whose signature did not verify, and by entrypoint/action endpoints for an always-send frame that failed verification. The signature is read off the frame envelope without trusting its `content`, so a corrupt frame can still be identified. - `"form-submit"`: Submits a form by ID. Optionally carries `action` and `method` overrides. Terminal (executed after non-terminal directives). - `"form-reset"`: Resets a form by ID. Terminal. Depending on the type, it will have other properties. ### Navigate Directive - `uri`: The URI to navigate to. ### Reenter Directive Carries no properties beyond `type`. Tells the client to re-fetch the page's entrypoint for the current URL — the same request the initial load makes, with the client's current slot headers — and apply its directives in place. Unlike `navigate` the page is not reloaded: the document, its listeners and client-side state (such as form dirty tracking) stay alive, so the page's entrypoint state is refreshed without disturbing the user's context. Execution is terminal but ordered: `reenter` runs after the response's form directives (`form-reset`, `form-submit`) and before `navigate`/`post`, so when both a `reenter` and a `navigate` appear in one response the entrypoint is refreshed first and the navigation then wins. The refresh is awaited before the remaining terminal directives run, which means the originating action's promise (and therefore `stm-clear-on-success` busy-state release and `statum.act` resolution) resolves only after the re-entered entrypoint state has been applied. A failed refresh (for example a network error) is logged to the console and swallowed: it neither rejects the action promise nor prevents a `navigate`/`post` in the same response from running. ### 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`. ### Transmit Directive - `key`: The key of the slot whose frame should be transmitted. - `retry`: A boolean (optional, defaults to `false`). Forces the client to transmit the named slot's current frame to the slot post endpoint immediately — exactly as a pre-flight does for a snapshot that has passed its `transmit_after` window (see [Snapshot freshness decision](#snapshot-freshness-decision)). The client POSTs the held frame, applies any `set` directives the slot endpoint returns (refreshing the snapshot), and then continues. If the client does not currently hold a frame for `key`, the transmission is skipped. When `retry` is `true`, the originating request (the one that returned this directive) is re-issued once the transmission and its response directives have been applied, so the server can re-evaluate the request against the freshly transmitted state. The retry carries rebuilt slot headers reflecting the transmitted (and possibly refreshed) snapshots. A retried response is processed like any other, so combining `transmit` with `retry` is how the server forces a state sync followed by a fresh evaluation of the same request. Transmit is not terminal: it runs in the same pass as `set` and `clear`. The actual transmission (and any retry) occurs after the response's other non-terminal directives have been applied and the page re-rendered. When a retry is performed, the original response's terminal directives (`navigate`, `reenter`, `post`) and `error` directives are not applied — the retried response supersedes them. ### Invalidate Directive - `signature`: The signature of the frame the server could not trust. When the server receives a frame whose signature does not verify (or whose snapshot has passed its `invalid_after` window) it returns an `invalidate` for that frame's `signature`. The signature is read straight off the frame envelope (`{content, signer, signature}`) without parsing or trusting `content`, so even a corrupt or untrustworthy frame can be identified. On receipt the client finds the held slot whose frame carries that signature and clears it — exactly as a `clear` directive would — and logs a console warning, so it stops re-sending a frame the server cannot accept. `invalidate` is emitted automatically by the slot post endpoint (`POST /_statum/slots`) for a posted frame that fails verification, and by the entrypoint/action endpoints for an always-send (`X-Statum-Slot`) frame that fails verification. ### Automatic transmit on missing slots When an entrypoint or action request references (in `X-Statum-Slot`) a slot the server no longer has in its cache, the server returns a `transmit` directive with `retry: true` for that slot's key *before* running the handler. The client responds by transmitting the held frame for that key to the slot post endpoint: if the frame verifies the server re-caches it and returns a fresh `set`; if it does not verify the server returns an `invalidate` for that signature and the client drops the slot. The client then re-issues the original request (with rebuilt headers reflecting the re-cached or invalidated slots), and the handler runs normally. This makes the client resilient to a server restart or cache expiry: a stale reference is recovered transparently (or the bad slot is dropped) without the handler ever seeing a slot it cannot trust. # 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 saved state snapshot frames to the server, either once they have reached their `transmit_after` time window (a pre-flight) or in response to a `transmit` directive. Sent as a JSON array of Statum Frames. For each frame the server verifies the signature: on success it re-caches the slot and returns a refreshed `set` directive; on failure it returns an `invalidate` for that frame's signature so the client drops the slot. Returns a JSON array of Statum Directives. ## Action Endpoints Action endpoints are served at auto-assigned GUID URIs (`/_statum/action/{guid}`); the `uri` an action reference carries points at its GUID. An additional `X-Statum-Private` header should contain the `private` data of the action if present. Returns a JSON array of Statum Directives. Like the entrypoint endpoint, an action endpoint returns `transmit(retry)` for any referenced slot not in the server's cache before running the handler. ## 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). ## Delivery and module structure The client ships as four ES5, dependency-free scripts (each a guarded IIFE adding to the same `window.statum` global), served as default resources: - `/_statum/resource/statum-core.js` — the slot protocol: storage and persistence, `X-Statum-Slot` assembly, entrypoint/action fetch, directive dispatch, SSE/SharedWorker delivery, the alert/confirm/notify override mechanism, and the seams modules register into (the internal bridge; the attribute registry itself is document-side). - `/_statum/resource/statum-document.js` — everything DOM: the render walk and the structural bindings (`stm-text`, `stm-html`, `stm-attribute.*`, `stm-class.*`, `stm-if/else`, `stm-for-…-in`, `stm-disabled`, `stm-action`), the attribute-handler registry (`statum.registerAttribute`), boot visibility, `stm-guard` links, `stm-on-error`/`stm-on-success` classes, and the form bindings (`stm-prefill`, `stm-clear-on-success`, `stm-clear-on-error`, the `statum.document` API). - `/_statum/resource/statum-markdown.js` — the CommonMark engine plus the `stm-markdown` binding, `stm-h-demotion`, and the programmatic `statum.markdown` API. Zero DOM dependencies in the engine itself. - `/_statum/resource/statum-binding.js` — client-side variables: `stm-global-{name}`, `stm-global-{name}-from-{attribute}`, `stm-local-{name}`, `stm-local-{name}-from-{attribute}` and `stm-declare-{name}` (see "Local definitions and client-side variables"). **Core is headless**: it applies state and directives without touching the DOM, so it is usable alone as a state layer (point `statum.config` at your endpoints before init). When document is loaded it reads the `stm-entrypoint`/`stm-slots`/`stm-channel`/`stm-worker` body attributes into `statum.config` and installs the presentation hooks. **Load order is a contract**: core first, then document, markdown, binding (markdown and binding are order-independent of each other). Document owns the registry; every later module registers its attribute handlers into it (a module loaded against core alone has its registrations queued until document arrives). Trailing modules may be omitted (a core + document page has the full state-binding walk minus markdown and variables — note `stm-markdown` and `stm-h-demotion` live in the markdown module, and the `stm-local-*`/`stm-global-*`/`stm-declare-*` variables live in the binding module; both are inert without their module). `statum-mkpstm` expands a legacy ``) and HTML-block constructs render as escaped *text*. This is the one intentional divergence from the CommonMark reference output, which passes raw HTML through. - **Link and image destinations are scheme-filtered**: relative URLs and the `http`, `https`, `mailto` and `ftp` schemes pass through; any other scheme (`javascript:`, `data:`, `vbscript:` …) keeps its link text but the `href`/`src` is replaced with `#`. - Tables, strikethrough (`~~x~~`) and task lists are not supported — they are extensions, not CommonMark. Conformance is verified against the official CommonMark 0.31.2 example suite by `tests/markdown/run.sh` (see *Getting started* for how to run it). ## Header demotion The attribute `stm-h-demotion` (e.g. `stm-h-demotion="1"`) demotes every child header tag (`

`–`

`) by the specified number of levels, capping at `
`. It runs after `stm-html` / `stm-markdown` so the rendered headers are present. Useful when rendering user-generated markdown inside a page that already has its own heading hierarchy. ## Attribute binding The attribute `stm-attribute.{name}` (where `{name}` is any attribute name) binds an attribute of the element to an expression. When the expression evaluates to a boolean, the attribute is toggled by presence (`true` adds it as a boolean attribute, `false` removes it); when it evaluates to any other value, the attribute is set to that value (coerced to a string). An `undefined` result removes the attribute. For example `` sets the `href`, while `` toggles `checked`. ## Conditional display The attribute `stm-if` shows or hides the element via the `hidden` attribute depending on the expression inside the statement, e.g. `
` or `
` Hidden branches stay in the DOM (they are re-evaluated on each render), so the `hidden` attribute must win over author CSS. The library injects `[hidden]{display:none !important}` into the document head at initialisation for exactly this reason — an ordinary rule like `li { display: flex }` would otherwise override the user-agent `[hidden]` styling and let hidden elements leak as phantom rows. The attribute `stm-else-if` can only exist as the next sibling of an element with either `stm-if` or `stm-else-if` and has the same type of value as `stm-if`. The attribute `stm-else` can exist only as the next sibling of an element with `stm-if` or `stm-else-if`. ## Guards A guard is a `` element in the document `` that redirects the page when a predicate fails. It is the standards-conforming, head-level way to express a page access/redirect policy (custom elements cannot validly live in ``, so the directive rides on ``, whose `rel` is the spec's extension point). ```html ``` - `require`: an eval predicate (the same expression language as `stm-if`) evaluated against the current state scope (`{ typeName: public }`). When it is falsy, the page redirects to `href`. - `href`: the URL to redirect to when the guard fails. - `pre-entrypoint` (boolean, optional): when present, the guard is also evaluated on the initial page load **before** the entrypoint request is made, using only the hydrated (persisted) state. When absent, the guard is evaluated only on the evaluate cycles (i.e. once the entrypoint has loaded and thereafter on every state change). Either way it is re-checked on every evaluate cycle — so logging out, for example, immediately bounces the user off a guarded page. Guards are a client-side, display-level convenience (like `stm-if`): they redirect the browser, they are not a security boundary. Authoritative access control lives at the entrypoint/action endpoints, which verify the signed slot data they receive. ## Conditional classes The attribute `stm-class.x` where `x` is any class name can be used to toggle classes on or off with a predicate, e.g. `
`. ## Disabled state The attribute `stm-disabled` takes a predicate expression. When truthy, the element and all of its descendant form controls are disabled; when falsy they are enabled, e.g. ``. ## Form prefill The attribute `stm-prefill` binds a form control's **value** to an expression, writing the control's *properties* (`.value`, `.checked`, `.selected`) — never its attributes. This is the counterpart to `stm-attribute.value`, which writes the value *attribute* (the server-rendered default): a prefill updates the live control so it never fights the user's typing. ```html ``` Semantics per control type: - **Text-like inputs** (`text`, `email`, `number`, `password`, `search`, `tel`, `url`, `date`, `datetime-local`, `month`, `week`, `time`, `range`, `color`, `hidden`) and `