server — the application, running without a screen
A server worker in Flux is the same application, headless: the same app block, the same
init / update / subs, the same journal — with the view left off. This page is the canonical
home for the server subscription catalogue (OnWebhook, OnSharedChange, OnJob, OnQueue), the
storage:shared collections and their access tiers, the SEO prerender projector, and the
re-derivation argument — what replay proves — by which a server recomputes a client’s verdict from
its journal. The capability catalogue these rows draw on lives in
host services; the client-side TEA core the plane reuses is
specified in The App plane.
New here? Start with Guide §11 — Determinism, replay and trust →
The server plane is sealed in design, and its rollout follows the v1 language, gated on its first consumer — the grader below. That maturity fact is stated once, here; the rest of the page describes the plane in the present tense, as the design it is.
The client plane executes on the device. That leaves a recurring list of needs — a leaderboard that can prove a score was earned, a forum that must store its posts somewhere, a webhook that must land somewhere, a public page a crawler can read, a push notification to a device whose application is closed — which resolve, on inspection, into a single design. This page is that design, and its claim is smaller than it sounds and larger in consequence: a server application is the same application, headless — not a second language, not a second runtime, not a second mental model.
The thesis — one plane, the consumers it unifies
| Consumer | What it needs from this plane |
|---|---|
| Grader / anti-cheat re-execution | run a client’s journal again, server-side, and issue a verdict |
| Shared durable state for third-party applications | a place a forum, a board or a room can live |
| Inbound webhooks | a URL a payment provider or a data vendor can call |
| Remote alert and push delivery | reach a device whose application is closed |
| Receipt and entitlement validation | verify a purchase somewhere the buyer cannot edit |
| Metered-feed billing | count what was consumed, where the counter can be trusted |
| Governance mirror | an append-only, hash-chained audit of a fleet |
| SEO prerender | serve a public page that is readable without executing anything |
| Licensed server-side execution | run a module on our infrastructure under its licence |
| Always-on collaboration sequencer | a single authority that orders concurrent edits |
Ten needs; one plane. The reason they collapse into one is that all ten are folds — an init,
a stream of messages, a total update — and that the one which must also render reaches for a
pure view and nothing more. The client already has that machine, and it already has an oracle
that proves two engines run it identically. The server plane is that machine, hosted.
A one-to-many media relay is the exception: it is media infrastructure rather than a fold, and it lands last in the plane’s own rollout.
The substrate — a hosted backend, no second vendor
The plane is built on the backend the platform already runs, and introduces no second one:
| Piece | Role |
|---|---|
| managed Postgres with row-level security | durable storage, with the ownership discipline enforced in the database |
| an edge function runtime that executes WebAssembly natively | the execution host for a headless application |
| object buckets | published sources and compiled modules, exactly as the client publication pipeline uses them |
| a realtime change feed | the transport under Sub OnSharedChange |
The deployment unit is not “a build”. It is the pinned build closure:
build-hash = source · lockfile (the transitive module-hash closure) · compiler version
· pinned routines · the canonical memory planThe rebuild gate — the check that already stands between a source change and a shipped module — becomes the deploy gate. A server never runs bytes that differ from the bytes the client verified. That single sentence is what the rest of this page rests on, and it is the reason the determinism story survives the move off the device.
Zero language change — an app without a view
A server worker is an app block with init, update and subs, a server capability set, and a
server-held journal. No new authoring model. No free-form server code. The Elm Architecture
(TEA) that the App plane describes is the whole of it, minus the
member that paints.
app grader {
capabilities: [ storage:shared ]
init(p) = { graded: 0, rejected: 0 }
update(m, msg) = match msg {
Scored(r) -> { model: m with { graded: m.graded + 1 },
cmds: [ SharedPut(r.runId, r.verdict, Written) ] }
Written(v) -> match v {
Ok(rev) -> { model: m, cmds: [] }
Conflict(rev) -> { model: m with { rejected: m.rejected + 1 }, cmds: [] }
Denied -> { model: m with { rejected: m.rejected + 1 }, cmds: [] }
QuotaExceeded -> { model: m with { rejected: m.rejected + 1 }, cmds: [] }
}
}
subs(m) = [ OnQueue("runs", Scored) ]
}That is a complete server. It has no view, and the compiler does not miss it: the five members
are optional, and a program that never renders never declares one.
Why this is not a convenience. Because
updateis pure, total and deterministic, two operational problems that normally need bespoke engineering stop being problems. Horizontal scaling: any instance can serve any message, because there is no hidden state to migrate — the Model is a fold. Crash recovery: an instance that dies is replaced by one that re-folds the journal from the last checkpoint. These are the client’s own mechanisms — checkpoint and journal — running on a machine with a different address.
The one thing the server must decide that the client never did
A pure fold is deterministic given an order. Two edge instances receiving concurrent deliveries do not agree on an order by being pure — purity is not a consensus algorithm, and treating it as one is the classic mistake.
So the ordering authority is the shared-storage substrate, and it is the only one. A
revision (rev) is assigned by the backend on write, never elected by an instance. Concurrent
subscription deliveries serialize through that assignment, and the journal each instance re-folds
is the authoritatively ordered one.
The server subscription catalogue
| Subscription | Delivers |
|---|---|
OnWebhook(path, C) |
an inbound HTTP call, decoded at the boundary against the schema the app declared |
OnSharedChange(scope, C) |
a change in a shared collection, as journaled messages |
OnJob(spec, C) |
a scheduled run — the server twin of schedule:wake |
OnQueue(name, C) |
a work item |
Ingress obeys the discipline the network pillar already established: a webhook payload is
decoded against a declared schema, and a broken required field is a typed message, never a
silent na that corrupts a Model three hops later. See net.
The server command rows
| Capability | Grants |
|---|---|
storage:shared |
the collection verbs below |
mail:send |
templated transactional email — templated, for the reason host services gives |
push:send |
Web-Push fan-out to registered devices — the delivery half of the client’s notify:* |
net:fetch |
egress, under the same per-domain grant model as the client, with a server egress budget |
pay:* |
receipt validation: the client’s Paid(receiptRef) hands over a reference, and the server is where it is checked and recorded |
Declared, not deployed ad hoc. A server application ships through the same publication pipeline as any other: source → compile → manifest sealed → provenance row. There is no back door where a script arrives on the server by another route, which is what keeps the audit-at-publication story true for server code as well as client code.
storage:shared — hosted collections with tiered access
Where does a forum live? Per-user storage (storage:own) is the wrong shape — a post is not
private. First-party collections are the wrong shape — they are first-party by construction. The
missing capability is a shared, durable collection an application can declare, with access
control an author can state and a reviewer can read.
The four tiers
| Tier | Who may read | Who may write | The shape it serves |
|---|---|---|---|
Own |
the owner | the owner | per-user documents — the storage:own semantics, hosted |
Room(roomKey) |
members of the room | members of the room | a session, a board, a collaborative document |
PublicRead |
everyone | the owner | a published article, a profile, a leaderboard |
PublicAppend |
everyone | everyone, under quota and moderation | the forum shape: anyone may post, nobody may rewrite |
An application declares its collections, its tier per collection, and its indexes at publication. The compiler turns each tier into row-level security policy in the database.
Why a closed set of tiers, and not a rules language. A general rule language sounds more expressive, and it is — including in the ways you did not intend. It is a second program, written in a second language, running in the security path, with no type system, no totality argument, no test harness and no reviewer who is fluent in it. Its failure mode is silent and total: a rule that reads one clause too permissively exposes a table, and nothing about that is visible in the application’s source.
Four named tiers have a different failure mode, and that is the whole point. The tier is in the manifest — inspectable before install, alongside the capability list. It compiles to a policy the database enforces below the application, so a bug in the application cannot bypass it. And because the set is closed, the mapping tier → policy is written once and verified once, rather than re-derived per app by whoever wrote the rules that day.
This is not a smaller feature dressed up as a safety property. It is the safety property: the reason we can say “an application cannot read another user’s row” is that no application can express the rule that would let it.
And, in the same spirit: never raw SQL. Not restricted SQL, not sanitized SQL — no SQL surface at all. There is no string that becomes a query, and therefore no injection.
The closed verb set
| Verb | Kind | Delivers through C |
|---|---|---|
SharedGet(key, C) |
Cmd |
the value, or NotFound |
SharedQuery(index, range, limit, C) |
Cmd |
a bounded row set over an index declared at publication |
SharedPut(key, value, C) |
Cmd |
a write verdict |
SharedDel(key, C) |
Cmd |
a write verdict |
Sub OnSharedChange(scope, C) |
Sub |
changes in scope, as journaled messages |
Every one of them carries a completion constructor — including the writes:
variant WriteVerdict { Ok(rev: num) | QuotaExceeded | Denied | Conflict(rev: num) }Why a durable write is never fire-and-forget. A command that changes durable, shared state and returns nothing gives an application exactly two options, both wrong: assume it worked, or poll. The verdict closes that:
Ok(rev)gives you the new revision,Deniedtells you the tier refused you,QuotaExceededtells you the truth about the quota, andConflict(rev)tells you somebody else got there first.
Conflict(rev) is the optimistic-concurrency arm. A write carries the revision it was based on;
if the stored revision moved, the write does not land and the application is handed the revision
that won. It re-reads, re-derives, and decides. What never happens is a silent overwrite —
the failure mode where two users both “succeeded” and one of them lost their work without anyone
being told.
Values are typed at the boundary
A stored value is decoded against the schema the application declared, with the same machinery the
APP plane already uses for persistence: a tolerant reader, SchemaMismatch when a provider drifts
outside the accepted version window, and a total migrate when the shape moves. Amounts may be
decimal — exact fixed-point, which is what money is. Quotas apply per application and per
user.
PublicAppend collections additionally carry host-side rate limits, report hooks, and owner or
administrator deletion. That is platform chrome, not script surface: an application cannot
grant itself moderation powers by declaring them, and an application cannot avoid moderation by
not declaring them.
Where the PublicAppend moderation surface splits — which report and delete hooks sit at the
platform level and which at the application level — is a line the plan draws at its first consumer,
not before it.
The bounded shared query
PAGE = 50
app board {
capabilities: [ storage:shared ]
init(p) = { rows: vec.fill(PAGE, na) }
update(m, msg) = match msg {
Load -> { model: m, cmds: [ SharedQuery("byTime", (0..PAGE), PAGE, Rows) ] }
Rows(rs) -> { model: m with { rows: rs }, cmds: [] }
Changed(e) -> { model: m, cmds: [ SharedQuery("byTime", (0..PAGE), PAGE, Rows) ] }
}
view(m) = col { for p in m.rows -> text(p.body) }
subs(m) = [ OnSharedChange("board", Changed) ]
}The Model’s row buffer is a fixed vec of capacity PAGE, and a short page leaves na in the
tail. There is no compaction step in that view, and none is possible: the language has no
filter, and where / mask are length-preserving — they blank an entry, they never remove
one (collections). What makes the view correct anyway is that iteration is
na-aware: a comprehension emits no child for a hole. So a page of eleven posts in a buffer
of fifty renders eleven rows, and the thirty-nine holes produce nothing — without a filter, and
without a length that depends on the data.
Three constraints are visible in that one command, and each is load-bearing.
The index is named, and it was declared at publication. You do not query by an arbitrary
predicate; you query an index that exists. This is the storage twin of the rule that there is no
filter in the language: a data-dependent scan has a data-dependent cost, and a plane that
promises bounded cost cannot host one.
The range and the limit are bounded, and the limit is const-folded. A query cannot return “all
the posts”. It returns a page, into a vec whose capacity the Model declared — which is why the
Model stays bounded ([ErrState] if it does not) and why the memory plan stays flat no matter how
large the collection grows on the server.
A change is a message. OnSharedChange delivers through the realtime feed, journaled like
every other subscription — so a collaborative view re-queries because it received a message, not
because it polled. The re-query is explicit in the sample above, and that is deliberate: the plane
does not secretly refresh your Model behind your back.
The first consumer — the grader
The leaderboard grader is the smallest end-to-end proof of the whole plane, and it is where the rollout starts.
A client plays; every input it received is in its journal. It submits the run. The server
instantiates the same module the client ran — same build-hash, same pinned closure — and
re-folds (init, [msg]). Then it compares.
Figure — the grader round-trip: the client submits its journal and claimed score, the server
re-folds
(init, [msg]) on the same pinned build closure, and the comparison of recomputed
verdict against claim is what makes the score checkable.
Two inputs make that re-fold trustworthy, and both already exist for other reasons:
- The seed is server-derived. Randomness enters an application through
OnRand(seed), and the seed is minted by the server, not the client. A player cannot search for a favourable seed, because they did not choose it. - Elapsed time is host-authoritative. The clock the run was scored against is not a number the client reported.
What this changes about replay. Until now, replay has been a developer feature: time travel in the editor, a test harness with no mocks, a bug report that reproduces exactly. The server plane makes it a security mechanism, without adding anything to the language. If a verdict is a pure function of
(init, [msg]), and the server can recompute that function on the same bytes, then a claimed score is checkable — and a forged one is a journal that does not fold to the claimed result. The anti-cheat property was not designed; it was inherited from determinism. That is the strongest argument this design makes for purity, and it is the reason the grader ships first: it proves the pinned closure and the headless application, and it needs nothing else.
The limit that argument has
“A forged run does not fold to the claimed result” is true for a score derived from the seed and from the elapsed time, because the server owns both. It is not true for every score, and the gap is worth stating precisely rather than leaving a reader to find it.
A score fed by a host-pushed outcome re-folds without divergence. Consider a game whose result
depends on something the host decided and handed to the application — a kernel result revealed as
a message, OnReveal -> Revealed(outcome). That outcome enters the journal as data. It is not
re-derived from the seed on the way back — the seed re-derives the messages that came from
randomness, and this one did not. So the server’s re-fold replays the outcome exactly as
written, including a forged one, and reaches the claimed result without a whisper of divergence.
The check passes. The claim is still a lie.
The same class covers a pixel. A reading taken from the client’s viewport — a distance, an angle, anything derived from pan and zoom — cannot be re-derived by a server that has no viewport. A forged pixel scalar re-folds cleanly for the same reason. Hence the standing rule: a pixel value never feeds a ranked verdict. It is a readout, or it is cosmetic. That is a language-level discipline, not a server one, and it holds whether or not this plane exists.
The server plane is what closes the first one. Because an outcome comes from a host kernel, and this runtime executes WebAssembly natively, the server can re-run that kernel itself and re-derive the outcome rather than trusting the journal’s copy of it. The re-fold then has a server-derived outcome, a server-derived seed and a host-authoritative clock, and the divergence argument closes over the whole verdict.
Until it does, an outcome-fed run has exactly two honest destinations, and no third: the host re-derives the outcome, or the run is local-score-only and excluded from the shared leaderboard. It is never accepted on the strength of the client’s journal alone.
The verdict is written to the score row with its receipt. Nothing about the game had to be written twice, and no “server-side rules engine” exists to drift out of sync with the client — the two are the same artifact, by construction.
What lands after it. Shared storage behind the first community feature; then the collaboration
sequencer and the governance mirror on the same runtime; then push and mail fan-out. The prerender
projector can land independently of all of them, because it depends on nothing in
storage:shared.
Whether an always-on sequencer can tolerate edge cold-start latency, or needs a persistent channel, is decided at its consumer, not here.
SEO prerender — a pure view projection
An application that renders through WebAssembly presents a crawler with an empty page. For a public course, a documentation site or a forum, that is not a trade-off — it is a disqualification.
The seam that fixes it needs no language change at all, because it is already there:
view( fold( init(p), prerenderMsgs(route) ) ) — pure · total · inside the I7 oraclefold and view are pure functions of data. The server can therefore run the same
WebAssembly module at publication time and project the resulting UiTree into sanitized,
semantic HTML.
The projector is the sanitizer’s server twin
UiTree node |
Projects to |
|---|---|
containers (col, row, grid, panel) |
semantic tags |
text, label |
text nodes |
image(assetRef) |
a resolved <img> |
| rich text | semantic prose |
| a link | a real anchor — crawlable |
chartView |
a static poster image |
It is the same closed catalogue the client sanitizer paints, targeting a different backend. An application cannot inject markup into a prerendered page for the same reason it cannot inject markup into a live one: no primitive produces markup, and the node set is closed.
Fixtures, gating, and freshness
Per-route fixtures. An application declares prerenderMsgs per route — the OnRoute payloads
that put the Model into the state that route should show. Each public route yields a static page.
Gated content renders its teaser, exactly as the access model already specifies: the crawler
sees what a signed-out visitor sees, which is the only honest thing to serve it.
Dynamic shared content. A pure fold cannot read storage:shared — purity is not negotiable
even here. So the projector run embeds a host-fetched SharedQuery snapshot in the route’s
fixture messages, and re-prerender of an affected route is triggered by the collection’s write
hooks, batched and debounced.
The consequence is stated plainly rather than glossed: crawlability of shared content is eventual, not live. A post is visible to a crawler shortly after it is written, not at the instant it is written.
Never per-request. There is no server compute per hit. A page is produced at publication, and regenerated incrementally when a write invalidates it. That is a deliberate ceiling on the operational surface of the whole plane: the request path stays a static file served from a CDN, and no traffic spike can become a compute bill or a cold-start latency.
Hydration is boot. The emitted page carries the standard application boot; the application starts and repaints from scratch. There is no DOM-state reconciliation step, because a deterministic repaint makes one unnecessary — the machinery other stacks need to reconcile a server-rendered tree with a client-rendered one has nothing to do here.
Locale variants — one prerendered page per locale, or a single page with an hreflang set — are
decided with the catalogue model in i18n, where that choice belongs,
not independently here.
The freshness policy for dynamic shared content — the acceptable lag, the batching window, and whether a high-churn collection opts out of prerender entirely — is a tuning the plane leaves to each collection, within the eventual-not-live ceiling stated above, rather than a constant baked into the plane.
Trust, tiers and quotas
Server applications are vendor-verified only at first. User-generated server code follows the same gate as any other capability that can spend our resources on somebody else’s behalf. This is a rollout decision, not a language boundary: the code that will eventually run there is the code that runs there now.
Everything else carries over verbatim from the client:
- Default-deny capabilities, aggregated transitively and capped by the grant:
manifest = ( ⋃ emit Cap over the transitive closure ) ⊓ the grant. A server row is a row like any other. - The manifest is inspectable before anything is deployed, and it is derived by the compiler from the emit sites — its only origin.
- Quotas and metering per application: CPU time, rows, egress. The accounting hooks that the network pillar defined for metered feeds find their billing consumer here — a cap reached is a circuit breaker, not a surprise invoice.
- Audit. The immutable host audit log gains a hash-chained server mirror, so governance across a fleet reads one journal discipline rather than two.
The receipt and entitlement schema for pay:* validation is co-designed with the payments family
in host services, where that surface is owned.
The third leg of determinism
Byte-identity has been a two-party claim: the interpreter and the compiled module agree, and the I7 gate proves it at every compilation. That base claim, the pinned routines behind it and the gate that enforces it are specified in Guarantees and Verification & reproducible builds; this section adds the leg the server plane is responsible for. The server plane makes byte-identity a three-party claim.
Figure — the three legs of byte-identity: the reference interpreter, the compiled module and the
server re-execution compute the same run and must agree — same numbers, same scene.
| Leg | The statement | Enforced by |
|---|---|---|
| interpreter ≡ WASM | the two client engines produce the same bytes | the I7 gate, at every compilation |
| WASM (client) ≡ WASM (server) | the server runs the same artifact | the pinned build closure = the deploy gate |
| interpreter ≡ WASM ≡ server | a verdict computed on a device and recomputed on the server is the same bytes | both of the above, together |
The third leg is not free. It holds if and only if two conditions hold, and both are mechanical:
The server executes the same pinned routines. Every place where two implementations could
legitimately differ — decimal arithmetic, transcendentals, the seeded integer generator,
collation, na-ordering, text metrics — is served by one routine, shared. Not the same
algorithm: the same code. A server that reached for its own platform’s decimal library, or its own
Math.log, would be a third implementation, and a third implementation is a third set of bugs.
The server re-links the exact dependency closure. The build-hash pins the lockfile, which pins the transitive module-hash closure. A server that resolved a dependency to a newer compatible version would be running a different program that happens to type-check — and it would disagree with the client on some input, eventually, in a way nobody could reproduce.
Why this is worth the discipline. Without both conditions, “byte-identical” is a promise between two parties who trust each other — a client and a compiler on the same machine. With them, it is a fact a third party can check: anyone holding the journal and the build-hash can recompute the result and compare. That is the difference between a determinism story you use to debug and one you can put a leaderboard, a receipt or an audit behind.
The honest residual. The floating-point caveat that qualifies determinism on the client — the
same-runtime clause for f64 series computed by the engine’s core paths — does not dissolve
by moving to a server; it applies where it applied before. What the third leg covers is the
domain that matters here: the fold and the view, and the decimal amounts inside them,
which are exact integer arithmetic through a shared, pinned routine. WebAssembly’s floating point
is specified bit-for-bit across machines, and both sides run the same closure — so the guarantee
is byte-determinism of the artifact, never a claim about the surrounding host engine.
Multi-region deployment is safe by the same closure argument — the same bytes run everywhere — so keeping regions in step is an operational discipline layered on a settled safety property, not an open language question.
See also
- The App plane — the TEA core, the journal, capabilities, and the two named Model-producer exceptions.
- host services — the client half: payments, notifications, and the resource-handle doctrine.
- net — typed ingress, egress grants, and the offline queue on the other side of the wire.
- Guarantees — what determinism promises, and the machine checks behind each promise.
- Compiler & runtime — the I7 gate, pinned routines, and reproducible builds.
- Packages & distribution — the lockfile and the dependency closure the deploy gate pins.