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.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)."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.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, referenced by path (e.g. typeName.deleteItem).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)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."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.
uri: The URI to navigate to.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.
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.
key: The key of the slot to clear.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.
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). 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.
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.
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). Notify runs in the same pass as set and clear.
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). 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, post) and error directives are not applied — the retried response supersedes them.
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.
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.
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.
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.
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 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.
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.
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:
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.transmit_after equals its as_at timestamp (always-send), the frame is sent in base64, e.g. X-Statum-Slot: eyAiY29udGVudCI6ICIuLi4uIiwgInNpZ25lciI6ICJzMSIsICJzaWduYXR1cmUiOiAiZXlBaVkyOXVkR1Z1ZENJNklDSXVMaTR1SWl3Z0luTnBaMjVsY2lJNklDSnpNU0lzSUNKemFXZHVZWFIxY21VaU9pQWlJbjA9In0=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.
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).
On startup, before fetching the entrypoint or evaluating any bindings, the library checks for a _statum_sdc session cookie:
"session"-scoped slots from localStorage, then set _statum_sdc=1 (a session cookie, so the browser drops it on close).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.
State snapshots can be cleaned up via statum.clear("typeName"). All snapshots can be cleared via statum.clear().
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": "..." } }.
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
Callbacks can be attached to statum using statum.addStateListener("typeName", callback) which calls callback with an object containing:
type: the type nameframe: 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.
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<boolean>. 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<boolean>. 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.The library exposes two no-arg callback hooks that fire before and after each render cycle (initial render, entrypoint hydration, set/clear, and realtime events):
statum.beforeEvaluate(fn) — registers a callback that runs before the DOM is re-rendered. Use it to prepare state, toggle loading classes, or perform any pre-render work.statum.afterEvaluate(fn) — registers a callback that runs after the DOM has been re-rendered and guards have been evaluated. Use it for post-render tasks like syntax highlighting, analytics, or DOM measurement.Callbacks take no arguments (the scope is built internally from held-slot state and is not mutable from hooks). They are isolated with try/catch; an exception in one hook does not prevent subsequent hooks or the render itself.
statum.beforeEvaluate(function () { document.body.classList.add('evaluating'); });
statum.afterEvaluate(function () { highlightCodeBlocks(); });
A global slot uses a deterministic key ("global:" + type_name) so every client references the same slot. This enables broadcast state — a live score, a system announcement, or any value the server owns and pushes to all connected clients.
Global slots are registered at startup and opt-in per page: each entrypoint or action that needs a global explicitly includes it via set_global (current value) and/or subscribe_global (live SSE updates). Pages that don't opt in never receive the global.
// Startup registration
statum.global<ScorePublic>("score", new ScorePublic { home = 0, away = 0 });
// Entrypoint — include the value + subscribe for live updates
return directives()
.set_global("score")
.subscribe_global("score");
// Action — update via update_held (client holds it from the entrypoint)
return directives().update_held<ScorePublic>("score", s => s.home += 1);
// Background (timer, webhook) — typed read-mutate-write
yield state_service.update_global_typed<ScorePublic>("score", s => s.home += 1);
Updates fan out via the existing SSE channel (every client subscribed to "global:score" receives the push). Clients that miss the push refresh on their next request via the stale-as_at auto-set mechanism. Global slots are public broadcast state only — the deterministic key is well-known, so there is no access control.
Live slot updates are delivered over the realtime channel (see 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.
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). The template is re-evaluated each time the state changes.
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.
The attribute stm-entrypoint can be used only on the <body> 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).
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. <div stm-content hidden>). The library removes it once the entrypoint has been processed, and also sets hidden itself during loading as a fallback for authors who do not pre-set it.
The attribute stm-text binds the element's innerText to the specified state value, e.g. <span stm-text="typeName.label">.
The attribute stm-html binds the element's innerHTML to the specified state value.
The attribute stm-markdown renders a state value as safe markdown: all HTML in the source is escaped first, then markdown formatting (headings, bold, italic, inline code, fenced code blocks, links, lists, blockquotes, horizontal rules, paragraphs) is applied. This is safe for untrusted user input — no raw HTML can be injected.
The attribute stm-h-demotion (e.g. stm-h-demotion="1") demotes every child header tag (<h1>–<h5>) by the specified number of levels, capping at <h6>. 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.
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 <a stm-attribute.href="user.profileUrl"> sets the href, while <input type="checkbox" stm-attribute.checked="user.agreed"> toggles checked.
The attribute stm-if adds or removes the element from the DOM depending on the expression inside the statement, e.g. <div stm-if="typeName.visable"> or <div stm-if="typeName.level > 5">
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.
A guard is a <link rel="stm-guard"> element in the document <head> 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 <head>, so the directive rides on <link>, whose rel is the spec's extension point).
<link rel="stm-guard" require="auth" href="/login" pre-entrypoint>
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.
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. <div stm-class.alert="typeName.waterLevel > 50">.
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. <button stm-action="typeName.submit" stm-disabled="!user.canSubmit">Submit</button>.
The attribute stm-action binds the element's primary action (click for buttons and links, submit for forms), overriding the default behaviour to instead perform the Statum action referenced by the given path into state. For example <button stm-action="typeName.deleteItem">Delete</button>.
Additional data can be attached to the action using one or more stm-data-{key} attributes, where {key} is the data key and the attribute value is its value, e.g. <button stm-action="typeName.deleteItem" stm-data-confirm="true">Delete</button>. These values are merged with any form input values when the request is sent.
The attribute stm-confirm may be added to an action element to require confirmation before it runs: its literal value is shown in a browser confirm() dialog, and the action is only performed if the user accepts, e.g. <button stm-action="typeName.deleteItem" stm-confirm="Delete this item?">Delete</button>.
The attribute will also disable the element (and any children) while the action is being processed, and restore the original state once completed (or on error). In addition to this, the attribute stm-busy-class can be used to specify one or more css classes to apply during this time, and stm-on-error can specify one or more css classes to apply while the element is in an error state (cleared the next time the action is attempted). The statum.act promise for the action also rejects on failure.
When used on a form, any form inputs are sent in the JSON or query string of the action, bound by their name, as they would for a regular native form submission.
The stm-for-{x}-in attribute (where {x} is the loop variable name) repeats the current element once for each item in the referenced array, and binds the loop variable for use in all stm- attributes on its descendants, and in all other stm- attributes on the element itself. For example:
<div class="list">
<div class="item" stm-for-product-in="typeName.availableProducts" stm-class.featured="product.featured">
<h3 stm-text="product.name"></h3>
<p stm-text="product.description"></p>
<span stm-text="product.price"></span>
<button stm-action="product.addToCartAction">Add to cart</button>
</div>
</div>
When the referenced array changes, the loop is re-rendered. If a stm-key="{expression}" attribute is present, items are matched across renders by the key value, preserving DOM identity (and therefore focus, selection, and input state) for items whose key is unchanged. When no stm-key is given, the loop falls back to a full re-render.
The attribute stm-def-{name}-as (where {name} is the variable name) defines a variable bound to an expression, available to the element and all of its descendants. The expression is evaluated in the current scope and its result is added to a child scope, so it can shadow a type binding or loop variable for the element's subtree. The value is re-evaluated on every render, so it stays reactive, and multiple stm-def-* attributes may be placed on a single element:
<div stm-def-total-as="cart.count * 9.99" stm-def-empty-as="cart.count === 0">
<span stm-text="total"></span>
<p stm-if="empty">Your cart is empty.</p>
</div>
Attributes and elements prefixed with pstm- are intended for server-side pre-rendering (see future.md) and must not reach the browser. On initialization the library scans the document for any pstm-* attributes or <pstm-*> elements and emits a single console.warn summarizing them, since their presence indicates a template that was not fully pre-rendered.