Suprnova Live is the framework's server-driven interaction engine. A Live component is a Rust struct whose state lives on the server, whose view is an Askama template, and whose actions run over a signed protocol from a small browser runtime that morphs the re-rendered HTML in place. There is no client-side state model to keep in sync, no build tool to install to use the shipped runtime, and no inline JavaScript in your documents.
This chapter covers the application-facing surface: authoring a component,
registering it, serving documents and islands, the security boundaries every
Live request crosses, uploads, asynchronous updates, assets, testing,
diagnostics, and recovery. Everything here uses only suprnova::live and
suprnova::view.
Quick start
A project created by suprnova new is Live-ready: it ships src/live/mod.rs
with an empty component registry and a routes() function, its bootstrap
binds the registry, and cmd/main.rs installs the routes. Scaffold a
component, then check it:
live:make writes src/live/counter.rs and templates/live/counter.html,
registers the component in src/live/mod.rs, and prints the next steps.
live:check builds your application and proves every registered view against
the integrated checker.
Authoring a component
use ;
/// A counter rendered by `live/counter.html`.
nameis the registered component name. Use a dotted, kebab-case name such asapp.counter; the CLI derives<package>.<kebab>.viewis the template identity, relative to the template root.#[public]fields are rendered and carried in the signed snapshot.#[model]fields additionally accept browser proposals throughlive:model.#[action]methods are the only entry points the browser can invoke. They receive validated arguments and may return typed outcomes such as a redirect or a flash.
Every field type must implement Default; a fresh island starts from those
defaults unless a mount hook says otherwise.
Views
Views are Askama templates. The template root is templates/ unless an
askama.toml names other directories, so live/counter.html lives at
templates/live/counter.html:
Count: {{ count }}
Increment
Directives use the closed live: grammar: live:click, live:submit,
live:model, live:upload, live:key, live:loading, and the rest of the
documented set. The checker proves every directive against the component:
an unknown action, an unknown model field, a raw safe filter, or an
accessibility violation fails live:check with the file, line, and column.
Documents that place islands are ordinary views declared with
#[suprnova::view]; the only unescaped value they accept is TrustedHtml
through the trusted_html filter.
Registration and bootstrap
src/live/mod.rs owns the registry and the routes:
use ;
/// Builds the registry of every Live component in this application.
Bind it during bootstrap so the server, the workers, and the suprnova live:* commands see the same components:
singleton;
The registry is immutable once the runtime assembles. A duplicate component
name or view, or a component whose actions need validation without a
validation port, fails registration with a typed RegistryError.
Routes
Router::try_live() installs the reserved namespace exactly once:
/__live/v1/action, /__live/v1/upload, the /__live/v1/async/* control
routes and WebSocket handshake, and the immutable /__live/v1/assets/*
routes. Startup fails if an application route can claim /__live.
The reserved request routes carry a strict policy: every request needs session, origin, CSRF, principal, tenant, and rate-limit facts. The framework records the session and the CSRF proof; your application attaches the rest with the route guard:
use Arc;
use Duration;
use ;
use InMemoryRateLimiter;
use ;
;
Install the routes from the entry point so the runtime and the mount catalog are ready before the first request:
new
.bootstrap
.try_routes
.run
.await;
Documents and islands
A document route declares its islands once, renders them through
LiveDocument, and emits the bootstrap tags:
use BTreeMap;
use ;
use ;
use ;
async
LiveMount::public_seeddeclares an island any visitor may render; its state is a reusable seed promoted to an instance on the first action.LiveMount::identity_bounddeclares an island that belongs to the current session and principal; the document route must authenticate.- Mount every island before
bootstrap, and callbootstraponce. The bootstrap emits the inert configuration element and the script tags for the ESM or classic strategy, adding the upload and asynchronous roles when a mounted component needs them and the Stimulus bridge on request. - The document template places
{{ bootstrap|trusted_html }}in<head>and each island where it belongs.
Security boundaries
Live never bypasses the framework's middleware. What each request needs:
| Fact | Recorded by |
|---|---|
| Session | SessionMiddleware |
| Origin and CSRF | CsrfMiddleware with origin verification enabled |
| Principal | AuthMiddleware on its authenticated branch |
| Tenant | LiveTenantMiddleware with your resolver |
| Rate limit | RateLimitMiddleware on its allowed branch |
The shipped runtime sends the Live media type and the browser's own
Sec-Fetch-Site header; it carries no session token. The CSRF middleware
verifies that proof for every Live request on its own, whatever origin policy
you configure, so a same-origin Live request passes with the stateless CSRF
disposition while a cross-site or header-less request falls back to token
validation and is refused. Ordinary routes keep token validation under the
default policy; using Live relaxes nothing else:
global_middleware!;
Anonymous visitors render public seeds, and they can act on them when the
guard uses AuthMiddleware::optional(): a signed-in principal is recorded, an
anonymous visitor continues, and the mount kind decides. A public seed then
promotes for the visitor's own session on the first action, while an
identity-bound island still refuses a request without principal evidence.
With AuthMiddleware::new() the guard answers 401 for every anonymous
request before any engine work. Identity-bound islands require a session and
a principal; the tenant is bound into the island's scope whenever your
resolver names one, and a resolver that cannot determine the tenant must
return an error rather than None. Every rejection is closed:
a 409 for a stale or tampered snapshot carries no body, and production
messages never include snapshots, tokens, cookies, or rendered HTML.
Uploads
Declare an upload policy on a model field:
use ;
The view binds the field with <input type="file" live:upload="avatar">. The
runtime creates, transfers, and completes the upload through
/__live/v1/upload; the file waits in quarantine until the declared finalize
action runs, when the framework hands it to your UploadFinalizer. Bind the
finalizer, and any scanner or validator, before the runtime assembles:
singleton;
Uploads are authorized per field and control through the gate. Define the
abilities live:<component>.upload.<field>.<Control> for Create,
Reacquire, Status, Queue, BeginTransfer, PutChunk, Complete,
Accept, BeginFinalize, CommitFinalize, Cancel, Reject, Expire,
and Fail.
A browser that lost its transfer grant reacquires it through a route your application owns outside the reserved namespace:
let router: Router = router
.try_live_upload_reacquisition?
.middleware
.into;
The route requires the same facts as an action, answers only the session and principal that created the upload, and returns a fresh grant with the current transfer state.
Asynchronous updates
A component declares the streams it listens to; the browser runtime subscribes over SSE or WebSocket and falls back to polling:
use ;
;
Define the ability live:<component>.stream.<name> for subscribers, then
publish from anywhere in the application:
let streams = resolve?;
streams..await?;
streams.refresh.await?;
A refresh tells subscribed islands to fresh-render; an event is delivered to the island's registered handlers. Polling is the ordinary fresh render: the island's state catches up whenever a transport is unavailable, but event payloads published in between are not replayed to their handlers, which the runtime reports as a degraded stream rather than a current one. A component that declares exactly one stream gets its island root subscribed for it; a component with several streams subscribes each through the runtime's registered calls.
Assets and no-build use
The framework serves the exact reviewed runtime artifacts at
/__live/v1/assets/<identity>/<file> with immutable caching, strong
validators, and integrity attributes in the bootstrap tags. A strict
script-src 'self' policy holds because documents contain no inline script.
To publish the same bytes to a CDN or a static directory:
The publication is atomic and refuses to replace a directory whose bytes
differ unless you pass --replace.
Testing
suprnova::live::testing prepares a router's runtime and mount catalog for
in-process tests. The application tests in app/tests/live_*.rs show the
complete pattern: an in-memory database, a seeded session cookie, the real
global middleware stack, and requests through handle_request:
let router = routes?;
let runtime = prepare_live_router_for_test?;
singleton;
Decode an island's snapshot from its data-suprnova-live-snapshot attribute,
post an action with the session cookie and Sec-Fetch-Site: same-origin, and
assert on the accepted render. A stale snapshot answers 409 with an empty
body; a missing principal answers 401.
Diagnostics and operations
suprnova live:checkproves every registered view;--allow-unprovedaccepts dynamic structures the checker deliberately makes no claim about.suprnova live:inspectreports the bound registry, configuration limits, installed upload capabilities, assembled runtime services, and the asset identity without exposing state or secrets.LiveConfigbounds request and response bytes and the trusted context lifetime; bind a custom one before the runtime assembles.- Errors carry closed kinds such as
live_document_context_rejectedandinvalid_live_bootstrap; telemetry labels are closed enumerations.
Recovery
- A
409tells the runtime to fresh-render the island; the operation is not replayed. - A closed asynchronous transport is retired and the runtime reconnects with a new transport generation; a stale generation is refused.
- A session that expires or rotates invalidates identity-bound work; the application exposes its sign-in path and the visitor continues from a fresh document.
Live runs complete without RenderCache. Caching Live documents is RenderCache's job; see RenderCache.
CLI reference
| Command | Purpose |
|---|---|
suprnova live:make <name> |
Scaffold a component and its view and register it |
suprnova live:check |
Prove every registered view with the integrated checker |
suprnova live:inspect |
Report safe runtime, registry, provider, and artifact state |
suprnova live:assets --out <dir> |
Publish the reviewed runtime artifacts atomically |


Comments 0