The serving plane
Every app on an Xe Computer — a static site, a container-backed service, an
app that actually lives on another device in your fleet — is reached through
one local address: http://<app>.localhost:5454. The serving plane is
the piece of the machine that answers that address. It is a small HTTP router
that runs on your own device, on loopback only, and decides — per request —
what to serve, what to refuse, and how to say so honestly.
This chapter sits one layer above Transport & identity (how bytes move between devices) and one layer below Shell & rendering (the browser surface, Prism, that frames what the plane serves). It is the request/response plane: the thing your browser talks to.
As-built truth. Citations are repo-relative in
agent54/xenon-launcheratmain @56e9457. The router source lives atcrates/launcher/assets/route-worker.js(request logic) andcrates/launcher/src/origin.rs(config generation + supervision); citations below are to those two files unless noted. Ports follow the current decisions d/100 (browser plane on 5454) and d/102 (internal peer-forward stays 5198, distinct). Where the design is honestly unfinished, this chapter says so and links the design review.
Primer: the words this chapter assumes
What is loopback /
localhost?127.0.0.1(and its IPv6 twin::1) is the address a machine uses to talk to itself; traffic never leaves the device. The namelocalhost— and, by RFC 6761, any name ending in.localhostsuch asphotos.localhost— always resolves to loopback, with no DNS lookup. That is why every Xe app gets its own<app>.localhostorigin for free: separate origins, all on your own machine.What is an origin? The web’s unit of isolation: scheme + host + port together (e.g.
http://photos.localhost:5454). Two pages from different origins cannot read each other’s data unless one explicitly grants access. The plane gives every app a distinct origin so the browser isolates them for us.What is workerd? The open-source JavaScript/Wasm server runtime that powers Cloudflare Workers — github.com/cloudflare/workerd. The Xe serving plane is a workerd process running one worker (
route-worker.js) as the origin router. We pin an exact workerd binary and supervise it; the launcher, not a cloud, owns it.What is Fetch Metadata? A set of browser-sent request headers (
Sec-Fetch-Site,Sec-Fetch-Mode, …) that tell the server how a request was initiated — same-site, cross-site, navigation, websocket. See W3C Fetch Metadata and this practical guide. The plane uses them as a cross-site-request-forgery floor.What is IWA / a controlled frame? An Isolated Web App is a signed, bundled web app the browser grants extra powers, running under a strict content-security policy. A controlled frame (WICG proposal) is the embedding primitive a super-app shell like Prism uses to host guest apps. Per d/101, ordinary Xe apps are not IWAs — they are plain http(s) framed by Prism. IWA headers are opt-in and reserved for super-app shells. This chapter’s “two-tier” section returns to it.
Zoom 1 — the big picture: one origin, one router, honest refusals
Three ideas carry the whole layer:
- One user-facing origin per app, all on 5454. The browser only ever
talks to
http://<app>.localhost:5454. The plane binds that port on loopback and nothing else is browser-facing (d/100). - A pinned, launcher-managed router. A supervised workerd process runs
route-worker.js. It is not a general web server you configure — it reads a generated route table and serves exactly what the table describes. - Fail closed, explain in plain language. Anything the router cannot
safely serve becomes a small
text/plainresponse that names the next action (“Runxe app install PACKAGE, then retry.”). There is no ambient cross-origin access and no silent success.
flowchart LR
Br[Browser / Prism frame] -->|http://app.localhost:5454| Plane
subgraph Plane["Serving plane — workerd on 127.0.0.1:5454"]
RW["route-worker.js<br/>origin router + refusal ladder"]
RT[(routes.json<br/>route table)]
RW --- RT
end
RW -->|static-digest| Store[["static-store<br/>read-only disk, no dotfiles"]]
RW -->|backend-proxy| Conn["backend-connector<br/>127.0.0.1:5199 → container"]
RW -->|peer| Fwd["peer-forward<br/>127.0.0.1:5198 → iroh tunnel"]
Sup["origin supervisor (origin.rs)<br/>pins + health-probes + restarts workerd"] -. manages .-> Plane
The launcher (origin.rs) generates the workerd config and the route
table, downloads and verifies the pinned workerd binary, and runs a supervisor
that health-probes the plane and restarts it on failure. workerd runs the
router. The router routes. That separation — a Rust supervisor owning a
JavaScript router — is the shape of the whole layer.
The ports, at a glance (all loopback)
| Port | Role | Source |
|---|---|---|
| 5454 | The Xe local plane — the single browser-facing origin http://<app>.localhost:5454 | fleet_catalog.rs:24 LOCAL_PLANE_PORT; origin.rs:34 |
| 5199 | Launcher-owned backend connector (workerd → container) | origin.rs:708 BACKEND_CONNECTOR_ADDR |
| 5198 | Peer-forward (workerd → iroh loopback forward), present but dormant until a peer route exists | origin.rs:728 PEER_FORWARD_PORT |
| per-peer | Fleet-catalog consumer forwards, one binding each | fleet_catalog.rs:133-138 |
d/100 fixes 5454 as the local consume plane only. d/102 keeps 5198 distinct
from 5454: a branch that unified them hit EADDRINUSE when the plane and a peer
consumer were co-resident (PR#57, closed unmerged). Only 5454 is browser-facing;
5199 and 5198 are launcher-internal plumbing and never an app origin
(origin.rs:704-714).
Zoom 2 — the route table: three frozen types
The router does not guess. For each request it looks up the app’s label in a
route table (routes.json, schema xe/origin-routes/v0) and finds a row whose
type tells it how to serve. There are exactly three types, and the
discriminator is frozen — this is decision-grade, set by P-148 (PR#36),
which also added an idempotence guarantee and a real-loopback HTTP conformance
suite:
flowchart TD
RT["RouteTable — schema xe/origin-routes/v0<br/>port + routes sorted by name"]
RT --> OR["OriginRoute<br/>name · package_id · type · digest? · static_serving? · backend? · peer? · iwa?"]
OR --> T{"type discriminator<br/>FROZEN by P-148"}
T --> SD["static-digest<br/>digest → read-only disk store<br/>SPA fallback, declared headers"]
T --> BP["backend-proxy<br/>BackendArm prefix/idempotent_get/connector<br/>→ connector 5199"]
T --> PR["peer<br/>PeerArm device/connector/origin_port<br/>→ forward 5198 / fleet forwards"]
BP -. "d/103-B: whole-origin overload<br/>empty digest + prefix '/'" .-> WO(("compose UI<br/>as backend-proxy"))
T -. "proposed additive 4th type<br/>(needs Luke ratification)" .-> CO(["container-origin"])
T ==>|unknown / missing| FC[["fail closed: 404 unsupported"]]
static-digest— the app is a bundle of files pinned by a SHA-256 digest. The router serves them from a read-only disk store (static-store,writable=false, allowDotfiles=false,origin.rs:803), with/→/index.html, optional single-page-app fallback, and MIME +X-Content-Type-Options: nosniffon every asset. This was the only populated arm at the freeze.backend-proxy— the app has a container backend. A declared path prefix is proxied through the launcher-owned connector on 5199; everything else can be served from a static frontend digest in the same row.peer— the app actually lives on another device. The router forwards to the iroh tunnel (see Peer serving & the fleet); if the publisher is offline the user gets an honest503naming the device.
Two properties make this safe to evolve. First, an unknown type fails closed
— any type not in {static-digest, backend-proxy, peer} yields a 404, never
an accidental serve (route-worker.js:272). Second, the digest field is
omitted when empty (skip_serializing_if, origin.rs:111), so adding backend
or peer arms leaves a pure static route’s bytes identical — which is exactly what
P-148’s idempotence tests guard.
The full field tables for the route table, the backend/peer arms, and the grant record are written up as a spec candidate — Route Table v1 in Part V.
Zoom 3 — the module: the life of a request and the refusal ladder
Inside route-worker.js, every request runs a fixed ladder. Each rung either
refuses with an honest error or passes the request down. This is the security
boundary as-built — the isolation model is same-origin with zero ambient
cross-origin access; the only way a cross-origin read materializes is a
short-lived, user-consented grant that is re-checked on every request and
every streamed chunk.
flowchart TD
A[Browser to app.localhost:5454] --> B["fetch(): strip x-xe-peer-request-origin"]
B --> C{"method CONNECT or TRACE?"}
C -- yes --> R400a[400 Bad Request]
C -- no --> D{"loopback host + /xe/api/index or /catalog?"}
D -- yes --> SVC[serve published index / merged catalog · 200]
D -- no --> E{"Host matches .localhost grammar<br/>AND == URL authority?"}
E -- no --> R400b[400 Bad Request]
E -- yes --> F{"path decode ok & no .. \\ NUL?"}
F -- no --> R400c[400 Bad Request]
F -- yes --> G["compute Fetch-Metadata:<br/>sameOrigin / crossSite / safe / upgrade"]
G --> H{OPTIONS preflight?}
H -- yes --> H1{grant or declared-static CORS?}
H1 -- grant --> P204[204 preflight]
H1 -- declared safe --> P204b[204 declared]
H1 -- neither --> R403a[403 no grant authorizes preflight]
H -- no --> I["routeFor(): read routes.json"]
I --> J{"route type known?<br/>static-digest / backend-proxy / peer"}
J -- unknown/missing --> R404u[404 unsupported / 503 missing table]
J -- known --> K{"cross-origin? origin && !sameOrigin"}
K -- yes --> K1{matching live grant?}
K1 -- yes --> K2{credentials ok?}
K2 -- no --> R403c[403 credentials not permitted]
K2 -- yes --> SG["serveGranted: static reads only,<br/>byte-ceiling + live revoke per chunk"]
K1 -- no --> K3{"declared static header covers<br/>safe non-upgrade non-backend path?"}
K3 -- yes --> STAT
K3 -- no --> R403x[403 cross-origin refused]
K -- no --> L{"same-origin but cross-site<br/>unsafe or websocket?"}
L -- yes --> R403f[403 cross-site refused · Fetch-Metadata]
L -- no --> M{"route.type == peer?"}
M -- yes --> PEER["servePeer: rebase origin/host,<br/>stamp; offline to 503"]
M -- no --> N{"path under backend.prefix?"}
N -- yes --> BE["serveBackend via connector 5199<br/>503 + Retry-After on failure"]
N -- no --> O{safe method?}
O -- no --> R405[405 · Allow: GET, HEAD]
O -- yes --> STAT["serveRouteStatic from digest<br/>SPA fallback; 404 if absent"]
STAT --> IWA{route.iwa?}
PEER --> IWA
IWA -- yes --> WRAP[wrap COOP/COEP/CSP]
IWA -- no --> DONE[response]
WRAP --> DONE
Walking the rungs (all route-worker.js unless noted):
- Entry & peer-origin unwrap (
:453). An internalx-xe-peer-request-originheader is read and stripped before any downstream code sees it (:455-460); it only selects response stamping for peer-served apps, and its hostname must match the request host and end in.localhostor it is ignored (:462-467). Authorization for a peer tunnel is the iroh bearer-ticket handshake, not this header (:458-459). - Method floor —
CONNECT/TRACE→400(:381). - Reserved service paths — on the loopback host (
127.0.0.1[:port]),/xe/api/indexserves the published app index and/xe/api/catalogthe merged fleet catalog (:384-385). These are the only non-.localhosthost the router answers. - Hostname guard —
Hostmust match the strict grammar^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.localhost(?::PORT)?$, andurl.hostnamemust equallabel + ".localhost"(:386-388). This closes host/URL-authority mismatch. IPv4 and IPv6 are separate binds. - Path safety —
decodeURIComponentin a try/catch (400on malformed percent-encoding), then reject\, NUL, or any..segment (:390-391). - Fetch-Metadata — derive
sameOrigin,crossSite,safe(GET/HEAD), andupgrade(websocket) fromOriginandSec-Fetch-Site(:392-399). - OPTIONS preflight — a matching live grant →
204; else a declared static CORS allowance for a safe GET/HEAD off a non-backend prefix → synthetic204; else403 No capability grant authorizes this cross-origin preflight(:400-413). - Route lookup —
routeFor()reads the table and fails closed on the frozen discriminator (:262-274): unknown type → unsupported; missing table → missing; no row and no dynamic peer → not-installed. - Cross-origin arm — with an
Originthat is not same-origin: a matching grant runs a credential check (cookies/Authorizationrefused unless the grant sayscredentials:"include") then serves static reads only with a live byte-ceiling; no grant means allow only a declared-static safe path, else403 Cross-origin request refused(:417-429). - Same-origin CSRF floor — a same-origin-looking but cross-site unsafe or
websocket request →
403 Cross-site request refused (Fetch Metadata)(:430-432). - Dispatch — honest
503/404for missing/not-installed/unsupported;peer→servePeer; a backend-prefix path →serveBackend; a non-safe method on a static route →405withAllow: GET, HEAD; otherwiseserveRouteStatic(:436-451). If (and only if) the route is IWA-marked, the response is wrapped with COOP/COEP/CSP headers (:214-220).
Two boundary facts worth stating plainly:
- A grant can never reach a backend. Grants cover static reads only; a
grant-authorized request to a backend prefix is refused
403(serveGranted,:304-306). The connector on 5199 is launcher-owned and never an app origin. - A grant owns its whole CORS envelope. Declared CORS headers cannot widen
what a request-level grant permits (
:204,:209-211), andguardStreamre-reads the grant table on every chunk, aborting on revocation, expiry, or a ceiling breach (:221-246).
The two-tier app model touchpoint (d/101)
Isolation between apps is delivered by giving each app its own origin and letting the browser (and Prism’s controlled frame) enforce the boundary. That is enough for the common case, so regular apps get plain http(s) from the plane and no COOP/COEP/CSP headers at all — this is d/101: “most apps should be regular apps that are just http/s.”
Only IWA-marked packages — today a short allowlist:
xenon/installer-sample and agent54/prism (is_iwa_marked_package,
origin.rs:683-685) — receive iwaHeaders: IWA_CSP
(frame-ancestors 'none', default-src 'self'), COOP same-origin, COEP
require-corp, and a dotfile manifest store. Prism, the super-app-tier shell,
is the canonical IWA; it frames the plane origin in its controlled frame and
the plane serves the framed guest as ordinary http. No ordinary app needs
signing, bundling, or IWA packaging to be served, listed, or loaded. See
App model & packaging and
Shell & rendering for the framing side.
There is a live tension here: the launcher still defaults to https on loopback with a name-constrained local CA, while d/101 frames regular apps as plain http(s). The design review treats this honestly as an open session question — it is not settled.
Honest-error behaviour today
The refusal ladder never throws an opaque failure at the user. Every refusal is
text/plain; charset=utf-8 with X-Content-Type-Options: nosniff, and every
body names the next action. A few load-bearing examples:
- Not installed →
404“This app is not installed. Runxe app install PACKAGE…”. - Backend cold or unreachable →
503withRetry-After: 2(“backend connector unavailable” / “backend not reachable”). - Peer offline →
503“This app runs on<device>, which is offline. Retry, or open it on that device.” plusCache-Control: no-store,Vary: *,xe-peer-liveness: offline(peerOffline,:39-44).
One deliberate absence: there is no 502 today. When the plane cannot reach
an upstream backend or peer, it synthesizes a 503 (a plane-side condition), and
a genuine upstream 5xx passes through unchanged. Whether the contract should
distinguish “plane can’t reach upstream” (503) from “upstream errored” (502)
is an open question the review
carries. The full status/trigger/body/header table is written up as the
Honest-error contract v1
spec candidate in Part V.
Supervision & pinning (why the plane is trustworthy to frame)
The router is only as trustworthy as the process behind the port. The supervisor
(run_origin_supervisor, origin.rs:978-1046) generates the config, calls
ensure_workerd(), and runs workerd under a health probe on 127.0.0.1:port
with an on-failure restart policy (max 10 retries, 1s→30s backoff). A Failed
service raises serving_plane_failed. ensure_workerd downloads the pinned
artifact, verifies size and SHA-256, extracts the binary, verifies the binary’s
SHA-256, and atomic-renames it into place; only linux-x86_64 and
darwin-arm64 are pinned, and any other target refuses honestly rather than
running an unverified binary (origin.rs:207-213, :832-906).
There is one seam this chapter must not gloss: port custody. The only
mutual-exclusion on 5454 is an advisory flock under the data root
(boot_service.rs:938-951) — it guards against a second cooperating Xe, not
against a foreign same-user process that binds 5454 first. If the plane is down,
another local process could hold the port and be framed by Prism as if it were
the plane. This is a real, named gap; the
review lays
out the options, including having Prism verify the plane’s identity before
framing it.
Next: the design review — desired design vs current implementation vs gap, the sovereignty-litmus table, and the open questions for Luke and Jan.