SDK

Provisioning & recipes

provision() is the high-level way to boot a worker-hosted VM and bring a catalog app up ready to run — installing it from the signed catalog, warming a snapshot, and handing you run/serve helpers. It carries no app-specific code: everything node needs to run is data in the node app's recipe.

Provision an app

javascript
import { Catalog, provision } from "@userland-run/nano-sdk";

const node = await provision(new Catalog(), "node@25.4.0", {
  wasm: await (await fetch("/nano/nano.wasm")).arrayBuffer(),
  swUrl: "/nano-sw.js",
  workerFactory: () =>
    new Worker(new URL("@userland-run/nano-sdk/worker", import.meta.url), { type: "module" }),
});

The VM runs in a Web Worker, so the UI thread never blocks. provision installs the app (and any recipe deps + extraApps) from the catalog, configures the recipe's warmup, and prewarms the snapshot off-thread.

Run and serve

javascript
// one-shot: run a file, stream stdout
await node.run("index.js", { onStdout: (c) => term.write(c) });

// inline code
await node.evalCode("console.log(process.version)");

// long-running server, surfaced to an iframe
const srv = await node.serve("server.js", { port: 3000, extraFiles });
iframe.src = await srv.url();  // /sw/3000/
srv.stop();

run/evalCode use the recipe's run templates to build the script; serve also wires the in-VM port to the service-worker preview bridge. extraFiles are injected per run (re-applied after each warm restore).

What a recipe is

An app recipe is app-specific provisioning data carried in the app's signed catalog manifest, so a generic runner needs zero per-app code. The node recipe lives in the catalog (catalog/recipes/node/recipe.toml); the SDK and your app stay node-agnostic.

Field
What it does
deps
catalog apps to install first
warmup
ELF + launcher + argv/env to snapshot at the /dev/__snapshot__ sentinel
run.fileScript / evalScript
templates that turn a run request into the /dev/__run__ payload
benignExitCodes
exit codes treated as success (e.g. node's shutdown 134)
outputFilters
regexes; matching line through end-of-output is stripped (e.g. a shutdown trace)
toml
# catalog/recipes/node/recipe.toml (excerpt)
[recipe]
benignExitCodes = [134]
outputFilters = ["WorkerThreadsTaskRunner", "Native stack trace"]

[recipe.warmup]
elfPath = "/usr/bin/node"
argv = ["node", "/launcher.js"]
env = { UV_THREADPOOL_SIZE = "0" }

[recipe.run]
fileScript = "process.mainModule.require('${file}')"

The layering

  • nano core — the RISC-V VM + a generic snapshot/restore primitive. Knows nothing about node.
  • SDK — generic transport, catalog install, serve bridge, and the provision() runner. Node-agnostic.
  • catalog — the node app + its recipe (the only place node-specifics live, as signed data).
  • your app — provision('node') and call run/serve. No setup code.
Pass `recipe` to provision() to override or supply one when an app's published manifest predates the recipe field.