Web stack · Pages and state

Statum

Page rendering and state management for web applications. HTML pages are composed and precompressed at build time; application state is signed, sealed in the browser and carried back with each request — so the server stays stateless between requests, with no session store and no database for ephemeral state. A small client script binds it all to the DOM.

Version
0.1
Licence
LGPL-3.0-or-later
Package
usm install statum
Language
Vala

Overview

The request, drawn out

Statum sits on Astralis and turns an HTTP server into an application framework. A request follows one shape, from first byte to bound DOM:

 Browser                          Server
 ───────                          ──────
 GET / ──────────────────────►   StatumPage (precompressed HTML)
 page loads, statum.js boots
 GET /_statum/entrypoint ────►   StatumEntrypoint → directives
 client applies directives        (set/clear/…)
 client binds stm-* attributes

 POST /_statum/action/{guid} ►  StatumAction → directives
 (with X-Statum-Slot headers)    (mutate state, push to channels)
The architecture at a glance, from getting-started.md.

The trick is where state lives. The server mints slots — units of application state, identified by a type name and a random key — signs them, and hands them to the browser. The client holds them and sends them back on every subsequent request. There is no session table to scale, no sticky routing to arrange; a restarted server with the same static keys verifies and continues where it left off.

A page's route is declared in its own HTML, its handler pairs with it by type, and the wiring is three lines:

void main(string[] args) {
    var app = new WebApplication(8080);
    app.add_module<StatumModule>();

    var statum = app.configure_with<StatumConfigurator>();
    statum.add_page<HomePage, HomeEntrypoint>();   // route from <pstm-uri>
    statum.action<BumpAction>();                   // GUID auto-assigned
    app.run();
}
From getting-started.md and example/Main.vala.

Features

What it hands you

  • Server-rendered pages. HTML composed from templates and fragments at build time by statum-mkpstm, precompressed with gzip, zstd and brotli and served with encoding negotiation and ETags.
  • Client-carried state. Signed snapshots — and for private data, encrypted ones — that the browser holds and returns, keeping the server stateless between requests.
  • Entrypoint hydration. After a page loads, the client fetches directives that set the initial slot data and bind the DOM.
  • GUID actions. POST and GET handlers auto-mapped to /_statum/action/{guid}, invoked from stm-action attributes, carrying strongly-typed private data.
  • Realtime updates. A SharedWorker-backed SSE channel pushes slot changes to every subscribed tab.
  • A topic system. External services — workers, webhooks, timers — trigger updates that load an entity and derive slot state for watching clients.
  • Three build tools. statum-mkpstm for pages, statum-mkres for precompressed resources, statum-genkeys for static signing and encryption keys.

Installation

Getting the library

With USM on your machine, one command acquires, builds and installs the library, its three command-line tools, C header, Vala binding and introspection:

usm install statum
Installs from the Astrologue package repository.

From source, the build lives at the repository root:

git clone <the Statum repository>
cd Statum
meson setup . build
ninja -C build
ninja -C build install
The same meson invocation the package's own build script uses.

Then compile against it with Vala's usual package mechanism:

valac --pkg statum-0.1 my-app.vala
The installed statum-0.1.deps resolves the Astralis, Invercargill and Inversion bindings for you.

Usage & documentation

Pages, slots and actions

Statum's documentation lives in its repository: getting-started.md is a full walkthrough of templates, slots, actions, the client script and the topic system, and model.md records the design. What follows is that guide in miniature.

A page, a template, an entrypoint

A page is an HTML file whose <head> declares its own route and the layout it wraps in — all pstm-* directives are build-time, processed by statum-mkpstm into a Vala class:

<!-- home.html -->
<pstm-uri>/</pstm-uri>
<pstm-template>main</pstm-template>

<p>Counter: <span stm-text="counter.value">0</span></p>
<button stm-action="counter.bump">Bump</button>
From getting-started.md — the route comes from <pstm-uri>, never passed by hand.

Templates name themselves with <pstm-name> and receive child content at <pstm-content>; fragments are inlined by name with <pstm-fragment>; and when a page is composed into its template the child's head tags merge into the parent's, so every layer can declare its own styles and scripts.

After the page loads, an entrypoint hydrates it — a handler that returns directives:

public class HomeEntrypoint : StatumEntrypoint {
    public override async DirectiveBuilder handle() throws GLib.Error {
        var counter = new CounterPublic();
        counter.value = 0;
        counter.bump = action_registry
            .author_private<BumpAction, BumpPrivate>(new BumpPrivate());
        return directives().set_typed<CounterPublic>("counter", Scope.PAGE, counter);
    }
}
From getting-started.md — the counter slot is page-scoped; the bump action reference carries sealed private data.

Slots and scopes

A slot's data is a snapshot — public plus private properties — wrapped in a frame the server signs. The client holds frames and returns them; private data is encrypted under a namespace (X25519 seal plus Ed25519 signature), so an action's private blob cannot be read by another action, and a stolen copy of public data cannot impersonate anyone. Four scopes decide how long a slot lives:

  • Scope.PAGE — in-memory, gone on navigate; transient page data.
  • Scope.FLOW — per tab via sessionStorage; a multi-step flow.
  • Scope.SESSION — per browser via localStorage; login state, preferences.
  • Scope.DEVICE — per device, long-lived settings.

Actions

Actions are not addressed by URL. The server authors a reference, embeds it in slot public data, and the client invokes it through an stm-action attribute. A typed action decrypts its private data before handle() runs:

public class BumpAction : TypedStatumAction<BumpPrivate> {
    public override async DirectiveBuilder handle() throws GLib.Error {
        var delta = request_private != null ? ((!)request_private).delta : 1;
        return directives()
            .update_held<CounterPublic>("counter", c => c.value += delta);
    }
}
From getting-started.md — update_held reads, mutates and re-signs the held slot, preserving action references.

Directives are a small, fluent vocabulary: set and set_typed sign state into the client, update_held mutates what is already held, clear drops it, navigate redirects, notify surfaces a message, subscribe joins a realtime channel — and terminal directives like form_submit finish a round trip by submitting a form by id.

The client script

statum.js is embedded in the library and served from /_statum/resource/statum.js — one script tag in the template, auto-initialising on DOMContentLoaded. Binding attributes do the rest:

<p>Counter: <span stm-text="counter.value">0</span></p>
<button stm-action="counter.bump">Bump</button>

<div stm-if="auth != null"> … </div>
<div stm-else> … </div>

<div stm-class.active="cart.count > 0"> … </div>
<div stm-markdown="post.body"></div>
From getting-started.md — text, HTML, CommonMark markdown, conditionals, class toggles and action calls.

Its markdown renderer targets the CommonMark 0.31.2 specification — all source HTML escaped first — and its JavaScript test suite runs under a vendored QuickJS, reporting conformance against the official example suite and failing below a 95% target.

Topics and global slots

A topic loads an entity from the database when triggered; a state modifier derives slot types from the payload and pushes them to subscribed clients over SSE. The example registers a topic that cycles an announcement every ten seconds, so two tabs side by side show the broadcast arriving in both:

statum.global<AnnouncementPublic>("announcement", announcement);
statum.topic<AnnouncementTopic>("announcement");
statum.topic_modifier<AnnouncementPreviewModifier, AnnouncementPreview>("announcement");

// trigger from anywhere — a worker, a webhook, a timer:
yield topic_registry.trigger("announcement");
From example/Main.vala and getting-started.md — global slots use deterministic keys for broadcast.

Keys and models

Signed frames and encrypted blobs need keys that outlive a restart. statum-genkeys prints a ready-to-paste statum section for web-config.json; without it the server generates random keys per restart, which is fine for development and logged with a warning.

Typed models serialise by GObject property introspection — scalars including int64 and bool round-trip as type-correct JSON primitives on both the public and the encrypted path, string[] properties come back as arrays, and Invercargill collections serialise as JSON arrays. The test suite pins exactly this:

var model = new LoginPrivate() {
    user_id = (int64)9007199254740993,
    is_admin = true,
    attempts = 3,
    score = 0.5,
    username = "bbarrow",
    session_no = (long)123456789012345
};

var wire = new JsonElement.from_properties(
    GObjectMapping.to_properties(model)).stringify();
check(wire.contains("\"user_id\":9007199254740993"),
    "public path: int64 serialises as an unquoted JSON number");
From tests/TestMain.vala — the suite runs through meson test and USM's test exec.

Package & source

The ledger

Version
0.1 (from MANIFEST.usm)
Licence
LGPL-3.0-or-later — free to run, study, share and improve; the LGPL keeps it free to link from any program.
Package
usm install statum
Provides
libstatum-0.1.so, statum-mkpstm, statum-mkres, statum-genkeys, statum-0.1.h, statum-0.1.vapi + .deps, statum-0.1.gir, statum-0.1.typelib, statum-0.1.pc
Needs at runtime
GLib and GObject, libsodium, libxml2, json-glib via the stack beneath — libinvercargill-1.so, libinvercargill-json.so, libinversion-0.1.so and libastralis-0.1.so.
Source
The repository Tilo15/Statum on the Astrologue forge (git over ssh, port 222). Free software: the licence guarantees your right to the source.