Architecture · self-hosting implementation
The application is WebAssembly. The browser boundary is Limen.
This site uses an F#/.NET WebAssembly reference engine.
The site uses the architecture it describes. Static HTML and CSS remain browser-native. Limen owns DOM bindings and browser effects. A tiny transport loads a .NET WebAssembly runtime. All application state, transitions, capabilities, obligations, effect interpretation, and view projection live in the engine — on this site, written in F#. Limen does not require F#: the engine role is defined by the boundary, not by a language.
Runtime path
Four layers. One authority boundary.
Every arrow crosses plain serializable data or browser-native structure.
Semantics, labels, layout, focusable controls, responsive presentation.
site/pages/*.html
Turns interactions into semantic events, applies projections, executes effects.
src/kernel/
Loads .NET and forwards one JSON message in, one JSON result out.
site/app/wasm-engine-transport.ts
Owns meaning: state, legal transitions, evidence, authority, obligations, capabilities, projection. F#/.NET on this site.
site/fsharp/Limen.Site.Engine/
The only C# in the site application is the tiny
[JSExport] marshalling shim required by .NET's source-generated
JavaScript export mechanism. It contains no application state or decision:
it forwards the serialized message directly to
Limen.Site.Engine.Dispatch.handle.
The contract
The boundary carries evidence, never application objects.
Defined by the npm package in src/protocol.ts.
type BrowserToEngineMessage =
| { kind: "Initialize"; protocolVersion; capabilities; location; handshake? }
| { kind: "Event"; event: SemanticEvent }
| { kind: "EffectResult"; result: EffectResult }
| { kind: "LocationChanged"; location: BrowserLocation }
| { kind: "CapabilityFact"; capability; version; fact };
// No DOM node.
// No callback.
// No shared object identity.
type EngineToBrowserMessage = {
view: ViewState;
effects: EffectRequest[];
cancellations: CorrelationId[];
};
// Projection + explicit requests.
// The browser never asks what they mean.
This separation is what lets the application move across a real WebAssembly boundary. The TypeScript package does not need F# types, and this site’s F# engine does not need DOM types. Both sides agree only on the serialized protocol — which is why any WASM language that can read and write it can take the engine role.
Language-neutral contract
One contract. Each guest enforces it in its own compiler.
No guest language is canonical. F# is the reference implementation, not a protocol requirement.
language-neutral Limen contract
│
┌─────────────┼─────────────┐
▼ ▼ ▼
F# WASM C# WASM Rust WASM
│ │ │
└─────────────┼─────────────┘
▼
browser host
(TypeScript today)
│
▼
browser APIs
| Property | Implemented on main today | Accepted and in progress |
|---|---|---|
| Contract source | The TypeScript protocol in src/protocol.ts; the F# engine mirrors it with handwritten types | One language-neutral source of truth (#15, #52) |
| Guest representations | TypeScript and handwritten F# | Generated F#, C#, Rust, and TypeScript bindings; generated code is read-only (#52, #55) |
| Guest compiler enforcement | F# engines build with warnings as errors, so an unhandled union case fails the build | Rust closed enums and results; C# generated closed hierarchies, nullable analysis, and analyzers (#55) |
| No raw browser objects across the boundary | The protocol is plain JSON-serializable data; engine-purity checks ban browser APIs in the engines | A mechanical check that protocol definitions contain no DOM or browser types (#51) |
| Version agreement | Initialize carries a protocolVersion | A contract fingerprint handshake before normal traffic; mismatch fails explicitly (#52) |
| Cross-language conformance | F# engine tests; kernel and federation tests | Shared golden vectors every guest must pass (#17, #32) |
JSON is the baseline wire format. The WebAssembly Component Model and WIT may become an additional backend, but Limen does not depend on them or on any one guest toolchain. Requirements and status are tracked in the capability parity specification (#15) and the guardrail specification (#51).
Authority
Who is allowed to know what.
If two layers disagree, the owner in this table wins.
| Layer | Owns | Must not decide |
|---|---|---|
| HTML | Document semantics, labels, native controls | Release legality, retry policy, state transitions |
| CSS | Presentation, focus appearance, responsive composition | Whether an action is legal |
| Browser | Paint, layout, caret, scroll, native input mechanics | Application state |
| Limen kernel | Binding, event transport, HTTP/storage/clipboard/navigation mechanisms | What an event, status code, route, or effect means |
| WASM transport | Runtime loading and serialization crossing | Any domain decision |
| WASM application engine F# on this site | Application truth, transitions, capabilities, obligations, interpretation, projection | Direct browser API access |
The hard cases
Why this is more than state management.
These are the cases the site now runs through F#.
Capability
Approval is not a button state
Tests and security evidence must both pass before F# projects approval capability. The DOM receives the result; it does not derive it.
Staleness
Older evidence cannot win a race
The F# engine carries the latest request identity. A late result for an older request is observable and discarded rather than silently overwriting current state.
Unknown effect
Timeout is not failure
After dispatch, a timeout becomes reconciliation-required state. No retry capability exists until authoritative evidence resolves what happened.
Missing capability
No browser escape hatch
If focus control or clipboard read is not in the protocol, application code cannot quietly reach for a DOM/global API. It becomes an explicit protocol decision.
Compute in code
Closed types make the legal state space visible.
This site’s release demo, written in F#, is not built from four independent flags. Deployment can be not started, in flight, deployed, failed, or reconciliation required. The transition function controls which moves exist.
- 01Evidence changes are legal only before approval.
- 02Approval exists only after both required evidence gates pass.
- 03Deployment exists only after approval.
- 04Only the matching correlation may complete an in-flight effect.
- 05OutcomeUnknown creates reconciliation, not retry.
Package versus site
What Limen ships, and what this repository demonstrates.
These are deliberately separate claims.
| Surface | Implementation | What it establishes |
|---|---|---|
| npm package | TypeScript kernel + protocol + reference engine; F# lifecycle CLI binaries | A browser boundary with explicit effects and a repository verification surface; it does not prescribe the engine language |
| Limen website | Static HTML/CSS + TypeScript Limen mechanics + F# WebAssembly application engine | The boundary can host a non-TypeScript engine in the same repository and drive nontrivial UI state |
| Chrona/time-entry consumer | F# domain/engine + .NET WebAssembly transport | An independent application has also crossed the same boundary |
None of those rows proves performance, development speed, defect reduction, or token savings. Those require controlled measurement. The evidence page keeps those boundaries explicit.
Verification
The site architecture is part of CI now.
A self-hosting demo that can silently fall back is not evidence.