Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-launcher at main @56e9457. The router source lives at crates/launcher/assets/route-worker.js (request logic) and crates/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 name localhost — and, by RFC 6761, any name ending in .localhost such as photos.localhost — always resolves to loopback, with no DNS lookup. That is why every Xe app gets its own <app>.localhost origin 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:

  1. 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).
  2. 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.
  3. Fail closed, explain in plain language. Anything the router cannot safely serve becomes a small text/plain response that names the next action (“Run xe 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)

PortRoleSource
5454The Xe local plane — the single browser-facing origin http://<app>.localhost:5454fleet_catalog.rs:24 LOCAL_PLANE_PORT; origin.rs:34
5199Launcher-owned backend connector (workerd → container)origin.rs:708 BACKEND_CONNECTOR_ADDR
5198Peer-forward (workerd → iroh loopback forward), present but dormant until a peer route existsorigin.rs:728 PEER_FORWARD_PORT
per-peerFleet-catalog consumer forwards, one binding eachfleet_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: nosniff on 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 honest 503 naming 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):

  1. Entry & peer-origin unwrap (:453). An internal x-xe-peer-request-origin header 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 .localhost or it is ignored (:462-467). Authorization for a peer tunnel is the iroh bearer-ticket handshake, not this header (:458-459).
  2. Method floorCONNECT/TRACE400 (:381).
  3. Reserved service paths — on the loopback host (127.0.0.1[:port]), /xe/api/index serves the published app index and /xe/api/catalog the merged fleet catalog (:384-385). These are the only non-.localhost host the router answers.
  4. Hostname guardHost must match the strict grammar ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.localhost(?::PORT)?$, and url.hostname must equal label + ".localhost" (:386-388). This closes host/URL-authority mismatch. IPv4 and IPv6 are separate binds.
  5. Path safetydecodeURIComponent in a try/catch (400 on malformed percent-encoding), then reject \, NUL, or any .. segment (:390-391).
  6. Fetch-Metadata — derive sameOrigin, crossSite, safe (GET/HEAD), and upgrade (websocket) from Origin and Sec-Fetch-Site (:392-399).
  7. OPTIONS preflight — a matching live grant → 204; else a declared static CORS allowance for a safe GET/HEAD off a non-backend prefix → synthetic 204; else 403 No capability grant authorizes this cross-origin preflight (:400-413).
  8. Route lookuprouteFor() 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.
  9. Cross-origin arm — with an Origin that is not same-origin: a matching grant runs a credential check (cookies/Authorization refused unless the grant says credentials:"include") then serves static reads only with a live byte-ceiling; no grant means allow only a declared-static safe path, else 403 Cross-origin request refused (:417-429).
  10. Same-origin CSRF floor — a same-origin-looking but cross-site unsafe or websocket request → 403 Cross-site request refused (Fetch Metadata) (:430-432).
  11. Dispatch — honest 503/404 for missing/not-installed/unsupported; peerservePeer; a backend-prefix path → serveBackend; a non-safe method on a static route → 405 with Allow: GET, HEAD; otherwise serveRouteStatic (: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), and guardStream re-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 installed404 “This app is not installed. Run xe app install PACKAGE…”.
  • Backend cold or unreachable503 with Retry-After: 2 (“backend connector unavailable” / “backend not reachable”).
  • Peer offline503 “This app runs on <device>, which is offline. Retry, or open it on that device.” plus Cache-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.