Run Linux and Node.js
in a browser tab.
One kernel routes each program to the right engine — a RISC-V VM running real BusyBox and Node.js v25 binaries, Node on the browser's JS engine, WASI commands, or sandboxed JS. No server. No install. Just a tab.
A Linux userland that runs client-side.
A shared kernel — an in-memory filesystem, a process router, a bus, capabilities and services — with four peer runners on top: a RISC-V VM that runs Linux ELF binaries unmodified, Node.js on the host JS engine, a wasm32-wasip1 tier, and a sandboxed JS engine. On the VM tier there are no shims: the actual BusyBox and Node.js binaries run instruction by instruction. The host Node tier trades that for speed — it implements Node's APIs over the browser engine, with documented divergences.
The right engine for each program.
A runner imports only from the kernel — never another runner. Cross-tier work goes through the router, the bus and the shared VFS, so a wasm tool, a Node program and a sandboxed script all see the same files (see it run).
riscv · slownode · fastwasm · fastboa · mediumOne sample per runner. Press Run.
These run in this tab on one NanoVM that boots on your first click — nothing is downloaded before. Edit the code and run it again. More in the examples.
BusyBox on the RISC-V VM
Each line runs in a shell on the emulated RV64GC CPU — the real BusyBox binary.
uname -m echo "hello from busybox" | tr a-z A-Z seq 1 5 | sort -rn | head -3
$ uname -m riscv64 $ echo "hello from busybox" | tr a-z A-Z HELLO FROM BUSYBOX $ seq 1 5 | sort -rn | head -3 5 4 3
Node.js on the host JS engine
nano.node() on nodert, the fast tier. It reports x64; the unmodified riscv64 Node binary runs on the VM — see Node.js in the browser.
const { createHash } = require("crypto");
console.log("node", process.version, process.platform, process.arch);
console.log("sha256:", createHash("sha256").update("userland.run").digest("hex").slice(0, 16));node v25.4.0 linux x64 sha256: 1cccb05f9290ba6d
Apply a sepia filter
A Rust image CLI compiled to wasm32-wasip1, on the browser's wasm engine. The result is shown below the output.
photon in.jpg out.png --filter sepia
$ photon in.jpg out.png --filter sepia photon: sepia 360x240 -> out.png (360x240 PNG)
Run the sandboxed script
Evaluated in the Boa sandbox with only fs and run granted — fetch does not exist in there.
// Runs in the Boa sandbox (nano.script). This engine was granted the `fs` and // `run` capabilities plus console — nothing else: no network, no DOM, no host // JavaScript objects. The value of the last expression (awaited if it is a // promise) comes back to the page as JSON. (async () => { const dir = "/work/project"; const files = nano.fs.list(dir).map((f) => f.name).sort(); console.log("files:", files.join(", ")); let total = 0; for (const name of files) { const lines = nano.fs.readText(dir + "/" + name).split("\n").filter(Boolean).length; total += lines; console.log(name.padEnd(12), lines, "lines"); } console.log("total:", total, "lines"); // `run` is a granted capability: a BusyBox applet on the RISC-V VM. const wc = await nano.run("wc -l " + dir + "/readme.md"); console.log("busybox wc -l:", wc.stdout.trim()); // Not granted: the global simply doesn't exist inside the sandbox. console.log("typeof fetch:", typeof fetch); return { files: files.length, total }; })();
files: app.js, readme.md, util.js
app.js 2 lines
readme.md 3 lines
util.js 2 lines
total: 7 lines
busybox wc -l: 3 /work/project/readme.md
typeof fetch: undefined
→ {
"files": 3,
"total": 7
}What the platform provides.
Route it, run it, share one kernel.
A Bellard-style pipeline.
The riscv runner — the fidelity oracle of the four tiers — is a monolithic exec() function whose dense dispatch compiles to a WASM br_table. #![no_std] Rust, fat-LTO'd into one function, talking to the host over a handful of imports.
By the numbers
Lighter than a VM. Closer than a sandbox.
NanoVM is a userland: it runs Linux programs over an emulated syscall layer, not a full kernel with devices. It is open source (AGPL-3.0-only OR LicenseRef-UEL; the SDK is MPL-2.0 OR LicenseRef-UEL) and needs no server. How it compares, with sources:
Run a real tool on a folder from your disk.
Pick a folder, run an open-source command-line program on it in this tab, save the results back. The files are processed locally and never uploaded.
Two Node engines, servers and previews — in this tab.
The Node-in-the-browser page runs Node on the host JS engine and the unmodified binary on the RISC-V VM, npm packages, an http server with a live preview and a cross-tier handoff — all client-side.
Run Node.js in the browser →What people build with it
From watching it run to embedding one.
Two on-ramps, one model. Try commands in the live terminal above. Then lift it into code with @userland-run/nano-sdk: a typed, ESM, zero-dependency package. Drop in <nano-terminal> for the whole UI, or call createHeadless for the same features without it.
// The whole terminal — files, editor, live preview, palette — as one web // component. Shadow DOM keeps its styles isolated from your page. import { defineNanoTerminal } from "@userland-run/nano-sdk/terminal"; defineNanoTerminal(); // <nano-terminal wasm-url="/nano/console.wasm" service-worker-url="/nano-sw.js" // style="height:560px"></nano-terminal> // …or the same features headlessly — no UI, just the capabilities: import { createHeadless } from "@userland-run/nano-sdk"; const s = await createHeadless({ wasmUrl: "/nano/console.wasm", serviceWorkerUrl: "/nano-sw.js" }); s.fs.writeFile("/work/server.js", serverSource); // files await s.installApp("node"); // signed catalog const { stdout } = await s.sh("ls /work | wc -l"); // a BusyBox shell line const srv = await s.serve({ node: ["/work/server.js"] }, { port: 3000 }); iframe.src = srv.url(); // routed through the service worker
Clone, build, run.
# clone & build the slim nano.wasm (2.46 MB, no bundled binaries) git clone https://github.com/userland-run/nano cd nano make build # run the test suite make test # serve the build locally on :8080 make serve