NanoVM runs the real, unmodified Node.js v25 binary — plus npm, TypeScript, ESLint and Prettier — entirely client-side. No server, no container, no install. An open-source alternative to WebContainers — and Node is just one of four runners on the platform; the wasm and sandboxed-JS demos run live below.
Pick a demo and hit run — the unmodified Node.js v25 binary executes it in the real terminal below, instruction by instruction, with npm packages, an HTTP server and a React build all served from inside the VM.
Node runs on the RISC-V VM; photon — a Rust image CLI compiled to wasm32-wasip1 — runs on the wasm runner, a faster, capability-scoped tier on the host wasm engine. Same kernel, same shared filesystem. Pick a filter; it decodes, transforms and re-encodes the PNG inside the tab.
The Boa runner is a Rust→wasm JavaScript interpreter that drives the VM with no guest process and no ambient authority — a fresh engine can touch nothing until you expose a capability. This script is granted read-only files and run, nothing else.
The kernel routes node to the fast host JS engine (its native arch), while the RISC-V VM runs a Linux userland — the same platform, two execution tiers, chosen per program by the router.
Other in-browser runtimes ship a reimplemented Node. NanoVM runs the actual Node.js executable — the same binary you’d install on a Linux server — instruction by instruction on an emulated RISC-V CPU. If it works on Node, it works here: the same APIs, the same versions, the same edge cases.
import { createNano } from "@userland-run/nano-sdk";
// boot the RISC-V VM (a BusyBox shell is built in), then install the real Node
// + TypeScript toolchain on demand from the signed catalog — 100% in the browser
const nano = await createNano({ image: { wasm: "/nano/console.wasm" } });
await nano.installApp("node"); // the riscv64 Node build, fetched + OPFS-cached
await nano.installApp("typescript"); // tsc, the same way
// write a TypeScript file into the in-memory filesystem
nano.fs.writeFile(
"/app/main.ts",
'const who: string = "RISC-V";\nconsole.log(`hello from ${who}`);',
);
// type-check + run it with the real tsc and node binaries
const { stdout } = await nano.shExec("cd /app && npx tsc main.ts && node main.js");
console.log(stdout); // hello from RISC-VNeed a tight run loop? Snapshot V8 once and restore it per run with the Node fast path.
// snapshot V8 once, then restore it per run for a fast loop
const rt = nano.nodeRuntime();
await rt.warmup();
await rt.run("console.log(process.version)"); // → v25.4.0fetch() (HTTP/HTTPS only, subject to CORS), so there are no raw TCP sockets yet. And, like any shared-memory tool, the page must be cross-origin isolated (COOP/COEP) — the SDK ships a service worker that arranges this for you.The SDK is a typed, ESM, zero-dependency package. Install it, point it at the wasm, and you have a Linux dev environment running in your users’ browsers.