An interface is not a document. Stop sending one.
EUI serves applications over HTTPS with no HTML, no CSS, no JavaScript and no JIT. The server sends an interface tree that is already resolved; a native Rust client applies it as binary patches and draws it on the GPU. Below is a real update, byte for byte.
The verbTwenty-three is set_text. There are sixteen ops in the whole protocol, and a client that meets a seventeenth refuses the frame.
The addressNode 141 already exists on the client. Nothing is re-sent around it: no row, no table, no page.
The payloadThirteen bytes of UTF-8 — the number a person actually reads. Nine bytes carried it.
The same table, measured twice
Fifty rows, four columns, keyed, six shared style records — an invoice table. Against it, the same table as HTML carrying the classes such a table really carries, unindented: the favourable case for HTML.
Both sides carry the same 2 337 bytes of text, which neither can
compress away, so the honest headline is the structure: 2 282 bytes
against 12 025, or 5.3×. The regression budget is set
at 4×, below what was measured, so drift does not trip it and a real
regression does. Source: crates/eui-proto/tests/size_budget.rs.
Numbers that come from a run
A budget nobody measured is a slogan. These are measured, and the ones that are still targets say so.
- 22 B
- a one-cell update, against a 40-byte budgetmeasured
- 201 B
- reversing fifty keyed rows — forty-nine moves, no subtree re-sentmeasured
- 0 %
- CPU at rest, and zero wakeups: the loop waits, it does not pollarchitecture, not a setting
- 80 ms
- launch to first pixeltarget
- 12 MB
- the stripped client, two variable fonts includedtarget — 15.38 MB today, and the miss is published
Three processes, and what each may do
Your application. It renders a tree of nodes, diffs it against the tree this session last received, and sends the patches. Written in Soli, or in anything that speaks the format.
Owns the display, the GPU, TLS and the clipboard. It never decodes a frame. When it is asked to close, it closes.
Everything that reads bytes a server chose: frames, layout, text, pictures, sound, bytecode. Confined by Landlock and seccomp — 36 system calls, and any other one kills it.
The client fetches assets by BLAKE3 hash, verifies them, and pins the publisher's Ed25519 key on first visit. A capability the manifest does not ask for has no code path at all.
What an application looks like
A component is two functions and a route. The handler takes an event
and the state and returns the next state; the view takes the state and
returns a tree, as plain data. Soli interns the atoms and the styles
once per session, diffs the tree against the one this client already
has, and sends the patches. This is the counter from
examples/counter-app, quoted whole.
# The handler: an event and a state in, the next state out. def counter(event_data) event = event_data["event"] count = event_data["state"]["count"] ?? 0 if event == "increment" {"count": count + 1} elsif event == "decrement" {"count": count - 1} else {"count": count} end end
# The view: state in, node tree out. Plain data. def counter_view(state) count = state["count"] ?? 0 with_state({"count": count}, column( {"pad": 6, "gap": 4, "bg": "surface.base"}, [ text("Counter", {"size": 4, "weight": "semibold"}), keyed("value", text(count.to_s, {"size": 7})), row({"gap": 2}, [ button("−", "decrement"), local_button("+", "state.count += 1; value.text = str(state.count)", "increment") ]) ] )) end
# config/routes.sl router_eui("counter", "live#counter", "live#counter_view")
surface.base and the type scale are roles it resolves itself, so the viewer's dark mode and font size are theirs, not the application's.
local_button carries a statement compiled to bytecode,
verified by the client before it first runs and metered by fuel: the
+ repaints in the frame you pressed it, then tells the
server, which confirms or corrects. Nothing a local handler does is
trusted — authorisation is never local.
column, text, button and the
ninety-four others are plain Soli functions returning hashes, and you
are meant to copy the file and change it. The
component reference has every signature,
the eight keys of a node, the thirty-seven style keys and the
twenty-eight colour roles.
What it will not do
Most of the safety here is subtraction. These are not defaults to be turned off; the client has no way to do them.
- Run downloaded native code, or JIT anything. Local handlers are verified bytecode on a whitelisted opcode set, metered by fuel.
spec 07 — bytecode - Resolve a cascade. Styles arrive already computed, as fixed 64-byte records; the client does one array lookup.
spec 02 — wire format - Read the clipboard, the camera or the filesystem without a capability the person granted.
spec 01 — transport - Report a keystroke outside a focused field, enumerate fonts, or read back a canvas. The fingerprint surface is close to nothing.
spec 08 — security - Fall back to plain HTTP. TLS 1.3, or no session.
spec 01 — transport
Where it actually is
The protocol, the client and the Soli integration exist and are tested. Mobile does not. The full account, crate by crate, is on the status page.
| Piece | State | Evidence |
|---|---|---|
| Wire format, decoder, fuzzing | Built | 64 tests |
| Layout, text, renderer | Built | golden pixels |
| Client: session, input, IME, accessibility | Built | 39 tests |
| Sound and moving pictures | Built | 26 tests |
| Sandbox: Landlock, seccomp, no core dump | Built | Linux only |
| Sandbox on macOS and Windows | Not started | — |
| Android, iOS | Not started | — |
A desktop artifact is 52.6 MB today against a 15 MB promise — it was 76 MB until the runtime it embeds learned to drop what an application never calls. The reason for what is left is that runtime, not the window: the interpreter alone is 10 MB. Both numbers are on the status page with the rest of the losses.