Architecture
The layers behind a box — kernel, filesystem, shell, commands, the Node runtime, and the network — and why none of it is WASM.
A Lifo box is a small stack of plain-TypeScript layers:
The Kernel owns the durable state — a virtual filesystem,
a process registry, a virtual network with
ports. The Shell parses and runs command lines against the kernel.
Commands are the userland — coreutils plus node, npm, git.
The Node compatibility layer is what lets real CLIs and dev
servers run unmodified. A service worker turns in-VM ports into
live previews in the browser.
Everything above is instantiated by Sandbox.create(), which wires the layers
together and hands you a small public API: commands.run(),
the fs helper, and direct access to kernel and shell.
Why not WASM?
This is the defining design decision, so it's worth spelling out.
The common way to run a Unix-y environment in the browser is to compile Linux
(or a language runtime) to WebAssembly and emulate a machine — syscalls, an ELF
loader, the works. Lifo deliberately does not do this. It maps the APIs
instead of the machine: the node:fs module, an HTTP server, a shell grammar,
process semantics, and child_process are reimplemented in TypeScript against
the kernel's in-memory state.
What that buys you:
- No image, no cold start. There's nothing to download and initialize. A box is a handful of objects; boot is sub-millisecond. A WASM Linux pays a multi-megabyte download and a warm-up before the first command.
- A tiny, tree-shakeable footprint. Lifo minifies like any other TypeScript dependency. There's no opaque binary blob riding along.
- One VM, two environments. Because it's just JavaScript talking to JavaScript, the identical box runs in a browser tab and in Node. Sandbox agent code client-side and it behaves the same server-side.
- Transparency. A failure is a stack trace in code you can read and patch, not a trap inside an emulator.
What it costs you:
- No arbitrary native binaries. Lifo can't execute a random compiled ELF. If a tool isn't JavaScript (or reachable through the compat layer), it doesn't run — though pluggable WASM runtimes for specific tools (ffmpeg, Python) are on the roadmap for exactly these cases.
The bet pays off because the workloads people actually want to sandbox — agent
code, npm/npx, dev servers like Vite and Expo, Git — are JavaScript talking
to Node APIs. Implement those APIs faithfully and the real ecosystem runs, with
none of the emulator tax.
The request path for a live preview
A concrete end-to-end example ties the layers together. When a dev server inside a box starts listening on a port:
- A command (say
node) starts an HTTP server via the Nodehttpshim, which registers the port on the kernel's network stack. - In the browser, a service worker intercepts requests to
/_sw/<boxId>/<port>/…and forwards them into the VM's HTTP handler. - The handler runs the dev server's request logic (all in-VM), and the response
streams back out through the service worker to an
<iframe>. - WebSocket upgrades (HMR) ride the same bridge, so hot reload works.
No part of that leaves the tab. See Networking, Live previews, and Protocols for the details.
Browser vs Node
The core is environment-agnostic; only the bottom "host bridge" layer differs.
In the browser, persistence is IndexedDB and previews go through a service
worker. In Node, the disk can be backed by the real filesystem via mounts, and
there's no service worker. os.platform() reports "lifo" so code can detect
the VM. See Browser vs Node.