userland.run70Try the demo
kernel + peer runners · WebAssembly · zero servers

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.

70
4 runners·2.46 MB slim wasm·Node v25·100% client-side
What it is

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.

Four runners, one kernel

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 · slow
Runs RV64 Linux ELF programs (BusyBox, Node.js) on an emulated CPU (Rust → wasm). fidelity oracle.
node · fast
Runs Node.js programs on the browser's JS engine (nodert). trusted.
wasm · fast
Runs wasm32-wasip1 commands on the browser's wasm engine. capability-scoped.
boa · medium
Runs untrusted JavaScript on the Boa interpreter (Rust → wasm). sandboxed.
See it run

One 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.

shell

BusyBox on the RISC-V VM

Each line runs in a shell on the emulated RV64GC CPU — the real BusyBox binary.

bash
uname -m
echo "hello from busybox" | tr a-z A-Z
seq 1 5 | sort -rn | head -3
loading…
Expected output
$ 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

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.

javascript
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));
loading…
Expected output
node v25.4.0 linux x64
sha256: 1cccb05f9290ba6d
wasm app

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.

bash
photon in.jpg out.png --filter sepia
loading…
Expected output
$ photon in.jpg out.png --filter sepia
photon: sepia 360x240 -> out.png (360x240 PNG)
Boa sandbox

Run the sandboxed script

Evaluated in the Boa sandbox with only fs and run granted — fetch does not exist in there.

javascript
// 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 };
})();
loading…
Expected output
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
}
Inside

What the platform provides.

BusyBox
echo, cat, ls, grep, sort, head, tail and the rest — the real BusyBox multi-call binary, running on the RISC-V VM.
Node.js v25
Two engines: the unmodified v25.4.0 riscv64 binary on the VM (faithful), or nodert on the host JS engine (fast).
TypeScript, ESLint, Prettier
tsc 5.9.3, ESLint 10.0.0 and Prettier 3.8.1 as signed catalog apps, running on the VM's Node.
Host-fetch networking
Outbound HTTP/HTTPS is brokered through the page's fetch(), subject to CORS — with a self-hostable CORS proxy.
App catalog
Install more tools — ripgrep, fd, jq and others — on demand from a signed, content-addressed catalog.
Sockets & epoll
socket, epoll and timerfd syscalls in the VM — enough to run an HTTP server inside it.
Threading
clone / futex-based cooperative multithreading, context-switched at syscall boundaries.
In-memory POSIX FS
One in-memory filesystem shared by every runner, with brk / mmap memory management in the VM.
ELF loader
Loads RV64GC Linux ELF binaries — segments, argv, envp and auxv, as Linux would.
HTTP preview
A service-worker bridge renders servers running inside NanoVM in a live preview iframe.
How it works

Route it, run it, share one kernel.

01
Route
The kernel's router picks a runner for each program — the RISC-V VM, the host Node engine, the wasm tier or the Boa sandbox — by ABI and policy.
02
Run
The chosen engine executes it: riscv interprets RV64GC through a dense exec() loop; node, wasm and boa run on the browser's engines.
03
Share
One kernel underneath — a shared in-memory VFS, a process table, a router and a bus. Runners talk only to the kernel, so tiers compose.
04
Bridge
~80 Linux syscalls in the VM, host-brokered fetch, sockets/epoll, and a service-worker preview bridge at the host boundary.
Inside the RISC-V runner

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.

INPUT
RISC-V ELF
segments · argv/envp
→
DECODE
Instruction decode
field extraction
→
CORE
exec() loop
br_table · #![no_std]
→
KERNEL
Syscall dispatch
~80 syscalls
→
HOST
MemFS / host
shared-memory FS
5 WASM imports·~30 exports·1 dependency (vte, no_std)·shared-memory FS protocol

By the numbers

2.46 MB
slim nano.wasm (emulator only)
4
runners · one kernel
~80
Linux syscalls
5 / ~30
wasm imports / exports
v25.4.0
Node.js
Where it fits

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:

Node.js in the browser

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

In-browser IDEs
Run Node projects and dev tools client-side — no backend build servers to operate.
Secure sandboxes
Execute untrusted code inside the browser's wasm boundary, or in the Boa sandbox with only the capabilities you grant.
Teaching & playgrounds
A Linux shell for courses and docs — reproducible, shareable, and reset by a reload.
Build your own

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.

javascript
// 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
Quick start

Clone, build, run.

bash
# 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