userland.rundocs70Try the demo
Getting started

Requirements

What a page needs to run NanoVM: cross-origin isolation (COOP/COEP headers or the shipped service worker), a current browser with shared WebAssembly memory, enough memory for the guest, and a CSP that allows the host-engine Node runner.

Cross-origin isolation

The VM's memory is a SharedArrayBuffer-backed WebAssembly.Memory (the emulator uses atomics for its threads). Browsers expose shared memory only when crossOriginIsolated is true, which takes two response headers on the HTML document:

text
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Cross-Origin-Embedder-Policy: credentialless also works and is easier when you embed third-party images or scripts. This site uses credentialless. createNano() throws a descriptive error when isolation is missing, unless you pass crossOriginIsolation: "ignore" (see Configuration).

SDK · JavaScript

Check this page

Reads the browser's own flags. On this site the headers come from the host (Cloudflare Pages _headers).

javascript
print("crossOriginIsolated:", globalThis.crossOriginIsolated);
print("SharedArrayBuffer:", typeof SharedArrayBuffer);
const mem = new WebAssembly.Memory({ initial: 1, maximum: 2, shared: true });
print("shared wasm memory:", mem.buffer instanceof SharedArrayBuffer);
loading…
Expected output
crossOriginIsolated: true
SharedArrayBuffer: function
shared wasm memory: true

Setting the headers

Host
How
Cloudflare Pages / Netlify
a _headers file: /* then Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp
Vercel
headers() in vercel.json or next.config
nginx
add_header Cross-Origin-Opener-Policy same-origin; and add_header Cross-Origin-Embedder-Policy require-corp;
Vite dev server
server.headers in vite.config
GitHub Pages and other hosts without header control
the service worker below

The service worker

The SDK ships nano-sw.js (@userland-run/nano-sdk/service-worker). Copy it to your public root. It does two jobs:

  • It adds the COOP/COEP headers to the navigations it controls, so hosts without header control still get isolation. A service worker only controls a page after it is installed, so the first visit must reload once.
  • It routes preview requests under <scope>sw/<port>/ to a page that registered a ServeBridge, which injects them into an HTTP server inside the VM. See Virtual server.
OptionTypeDefaultDescription
swUrlstring—URL of nano-sw.js. Required.
scopestringthe worker's directoryService-worker scope. Preview URLs are <scope>sw/<port>/….
reloadIfUncontrolledbooleantrueReload once when the worker is not yet controlling the page, so its headers apply. A sessionStorage guard prevents reload loops.

registerNanoServiceWorker(opts) returns { registered, controlling, crossOriginIsolated, reloading }; when reloading is true, stop and let the page reload. ensureCrossOriginIsolated(opts) wraps it: it resolves true once the page is isolated, false while a reload is in flight or when service workers are unavailable. When the page is already isolated it still registers the worker (for previews) but never reloads.

SDK · JavaScript

Register the service worker

This page is already isolated by its headers, so ensureCrossOriginIsolated() only registers the worker and returns true. reloadIfUncontrolled: false keeps the low-level call from reloading.

javascript
const status = await sdk.registerNanoServiceWorker({ swUrl: "/nano-sw.js", reloadIfUncontrolled: false });
print("registered:", status.registered, "| isolated:", status.crossOriginIsolated, "| reloading:", status.reloading);
print("ensureCrossOriginIsolated:", await sdk.ensureCrossOriginIsolated({ swUrl: "/nano-sw.js" }));
loading…
Expected output
registered: true | isolated: true | reloading: false
ensureCrossOriginIsolated: true

Browsers

NanoVM needs WebAssembly threads (shared memory and atomics), module workers, DecompressionStream (catalog installs) and service workers (previews and header injection). The site's test suite runs on Chromium (Playwright). Current Firefox and Safari releases support the same primitives, but we do not run the suite on them yet, so treat them as untested.

Feature
Needed for
crossOriginIsolated + shared WebAssembly.Memory
every VM
Service workers
previews (Serve mode) and header injection
Module workers
the Web Worker transport and the host-engine Node runner
Origin private file system (OPFS)
the catalog's chunk cache (falls back to memory)
WebGPU
optional: the terminal renders with WebGPU and falls back to a 2D canvas

Memory

ramMB is the size of the VM's linear memory. The images declare 192 MiB initial and 2 GiB maximum memory (--initial-memory=201326592 --max-memory=2147483648), so the useful range is 192 to about 2000. BusyBox work runs in 256 to 512 MB. Node.js on the RISC-V runner needs about 1.8 GB, which is why createNano() defaults to 2000 - wasmMB - 20 when you omit ramMB (see Configuration). Each VM is a separate allocation, so two 1.8 GB VMs in one tab need about 3.6 GB.

Content Security Policy

The host-engine Node runner loads its module tree from dist/vendor/runners/node (or /vendor/runners/node at your site root) and evaluates modules, so a CSP must allow 'unsafe-eval' for it. Without it, engines.node: "host" rejects with ERR_NODE_HOST_RUNTIME_UNAVAILABLE and "auto" falls back to the RISC-V runner. Shared memory also requires 'wasm-unsafe-eval' (or 'unsafe-eval') for WebAssembly compilation.

Every example and feature page on this site runs under exactly these requirements.

Network

The default catalog fetches from cdn.jsdelivr.net and data.jsdelivr.com; with COEP require-corp these responses must be CORS-enabled, which jsDelivr does. Guest programs reach the internet only through the host's fetch() (see Networking).