EUI applications without a browser
Thesis§00

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.

Frame · Batch · one op 22 bytes on the wire
03frame: batch 14length 20 02seq 2 011 op 23set_text 8d 01node 141 01inline text 0d13 bytes 31 20 39 38 34 2c 20 34 32 20 e2 82 ac“1 984, 42 €”
Invoice total 1 984,42 €

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.

Wire§02

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.

EUI 4 619 B
HTML 14 362 B

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.

Budgets§10

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
Session§01

Three processes, and what each may do

Server

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.

Window

Owns the display, the GPU, TLS and the clipboard. It never decodes a frame. When it is asked to close, it closes.

Worker

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.

Soli§06

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.

live_controller.sl
# 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")
what the client draws
Counter 42 + No stylesheet reached the client: 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.
− asks the server + answers locally

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.

Refusals§08

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
Standing§09

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.

PieceStateEvidence
Wire format, decoder, fuzzingBuilt64 tests
Layout, text, rendererBuiltgolden pixels
Client: session, input, IME, accessibilityBuilt39 tests
Sound and moving picturesBuilt26 tests
Sandbox: Landlock, seccomp, no core dumpBuiltLinux only
Sandbox on macOS and WindowsNot started
Android, iOSNot 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.