Quick start
Install @userland-run/nano-sdk, serve the page cross-origin isolated, boot a VM and run your first command. The samples below are the same calls, running in this page.
1. Install the SDK
npm install @userland-run/nano-sdk
The package is ESM-only, fully typed and has no runtime dependencies. The NanoVM runtime is vendored inside it. Licensed MPL-2.0 OR LicenseRef-UEL.
2. Serve the WebAssembly images
The SDK does not bundle the VM images; you serve them as static files. Two builds matter:
console.wasm (built as nano.busybox.wasm)nano.wasm (slim)Build them from the nano repository (make build-busybox, make build, make build-boa) or copy them from this site's /nano/ directory. Also copy the SDK's service worker (@userland-run/nano-sdk/service-worker, i.e. static/nano-sw.js) to your public root, and its dist/vendor/ directory to /vendor/ (the host-engine Node and wasm runners load their modules from there when a bundler has moved the SDK).
3. Make the page cross-origin isolated
NanoVM needs a shared WebAssembly.Memory, which browsers only allow on cross-origin isolated pages. Send these two headers, or let the shipped service worker add them. The details are on Requirements.
Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp
4. Boot and run
import { createNano, ensureCrossOriginIsolated } from "@userland-run/nano-sdk";
if (!(await ensureCrossOriginIsolated({ swUrl: "/nano-sw.js" }))) {
// the page is reloading once to pick up the headers
} else {
const nano = await createNano({
image: { wasm: "/nano/console.wasm" },
ramMB: 512,
engines: { node: "host" },
});
const { stdout } = await nano.shExec("echo hello from RISC-V");
console.log(stdout);
}This page boots exactly that configuration when you press Run. In every sample, nano is the booted instance, print() writes to the output pane and sdk is the imported module.
Run a shell pipeline
fs.writeFile seeds a file synchronously; shExec runs a full sh line with pipes on the RISC-V CPU and resolves with { exitCode, stdout }.
nano.fs.writeFile("/work/scores.txt", "ada 92\nlinus 87\ngrace 99\n");
const r = await nano.shExec("sort -k2 -n -r /work/scores.txt | head -1");
print(r.stdout.trim(), "(exit " + r.exitCode + ")");grace 99 (exit 0)
Run a Node.js program
A plain Node.js script. On this page it runs on the host JS engine (engines: { node: "host" }), so it starts in milliseconds.
const os = require("os");
const path = require("path");
console.log("node", process.version, "on", os.platform());
console.log(path.join("/work", "src", "..", "index.js"));node v25.4.0 on linux /work/index.js
Keep state between commands
The SDK Shell tracks the working directory and exported variables across lines, like a terminal session.
mkdir -p project && cd project export GREETING=hi echo "$GREETING from $(pwd)"
hi from /work/project
5. Next steps
- Every boot option: Configuration.
- Pick a runner per program: Runners & engines.
- Install tools from the signed catalog: App catalog and the app list.
- Drop in a full terminal UI: Embedded terminal.
- Complete apps built on the SDK: the examples gallery, starting with hello-node.
Building NanoVM from source
You only need this to change the emulator itself. The nano repository builds with a nightly Rust toolchain (rust-toolchain.toml pins nightly and the wasm32-unknown-unknown target).
git clone https://github.com/userland-run/nano cd nano make build # slim wasm/nano.wasm make build-busybox # wasm/nano.busybox.wasm (= console.wasm) node test/run.mjs runners/riscv/images/busybox --cmd echo "Hello from RISC-V"
See the build guide for every make target and the command-line runner for test/run.mjs.