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:
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).
Check this page
Reads the browser's own flags. On this site the headers come from the host (Cloudflare Pages _headers).
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);crossOriginIsolated: true SharedArrayBuffer: function shared wasm memory: true
Setting the headers
_headers file: /* then Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corpheaders() in vercel.json or next.configadd_header Cross-Origin-Opener-Policy same-origin; and add_header Cross-Origin-Embedder-Policy require-corp;server.headers in vite.configThe 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.
| Option | Type | Default | Description |
|---|---|---|---|
swUrl | string | — | URL of nano-sw.js. Required. |
scope | string | the worker's directory | Service-worker scope. Preview URLs are <scope>sw/<port>/…. |
reloadIfUncontrolled | boolean | true | Reload 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.
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.
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" }));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.
crossOriginIsolated + shared WebAssembly.MemoryMemory
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).