◆ Flux

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 plan

The 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.

FLUX
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 update is 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:

FLUX
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, Denied tells you the tier refused you, QuotaExceeded tells you the truth about the quota, and Conflict(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

FLUX
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.

The grader round-trip: a submitted journal, re-folded on the same pinned build closure, compared to the claimed score 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:

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 oracle

fold 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:

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.

Determinism as a tripod: three independent evaluators agree byte for byte 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