Live previews
How a service worker turns an in-VM port into a live preview in the browser, with HMR and clean in-app routing.
In the browser, a dev server running inside a box is served to an <iframe> by a
service worker. The ServiceWorkerBridge connects the page's service worker
to the VM, so a request to a preview URL is answered by the in-VM
HTTP handler — no network hop leaves the tab.
The request path
- The page registers
`/sw.js`and connects the bridge to the box. - An
<iframe>points at`/_sw/<boxId>/<port>/`— the entry URL for the server listening on that port inside the VM. - The service worker intercepts requests under
`/_sw/<boxId>/<port>/`and forwards each as aVirtualRequestinto the VM's handler for<port>. - The response streams back through the worker to the iframe.
The <boxId> in the path is what lets multiple boxes coexist on one page:
each box owns its own kernel and ServiceWorkerBridge with a stable id, so the
worker routes `/_sw/<boxId>/<port>/` to the right box's handler. Because the
worker then routes a client's subsequent requests by its client id (not by
re-parsing the path), only the entry URL needs the prefix — the app itself can
fetch from `/` normally.
Clean routing at /
Serving an app under a `/_sw/<boxId>/<port>` prefix would break client-side routers
(Expo Router, React Router) that expect to live at `/`. The service worker
injects a small path shim into every VM-served HTML document: before app code
runs, it history.replaceStates the location to strip the prefix. The app then
matches `/` like it would on a real dev server — with zero app changes.
To keep top-level preview-tab reloads working, the shim also saves the current
in-VM path to sessionStorage on pagehide (top-level windows only, so an
iframe doesn't hijack its parent), and the playground restores it on boot.
HMR
Dev servers push hot updates over a WebSocket. Those upgrades ride the same bridge: the worker proxies the WebSocket to the in-VM server's socket, so Vite/Metro HMR and Fast Refresh work inside the preview. See Protocols for the framing.
Production note
Previews rely on the service worker controlling `/`. In production the host
site must serve the worker with a Service-Worker-Allowed: / header so it can
claim that scope. Locally (and in the playground) this is already set up.
Gotcha
A service worker only exists in the browser. Server-side in Node there's no
worker — you reach an in-VM port directly through the network
layer or a tunnel. After the sw.js
changes, a client needs one reload for the new worker to take control.