../

WASI & components

WebAssembly outside the browser: WASI versions and capability-based security, runtimes, the Wasmtime CLI, WIT and the component model, the canonical ABI, tooling for Rust, JS and Python, composition and registries, and running components in Node and the browser with jco. Language details are in Rust and Emscripten.

WASI versions

VersionReleasedModelStatus (September 2026)
Preview 0 (wasi_unstable)2019core module importsobsolete
Preview 1 (wasi_snapshot_preview1)December 2019core module imports, POSIX-like, pointers into linear memoryfrozen; still the most widely supported (Node, Bun, Wasmer, WasmEdge, WAMR, wasi-sdk, Go, Zig)
WASI 0.2 (Preview 2)25 January 2024components with WIT interfaces (wasi:cli, wasi:http, wasi:filesystem, wasi:sockets, ...)stable; patch releases 0.2.1 to 0.2.12 on a two-month train, now concluded
WASI 0.3 (Preview 3)11 June 2026 (0.3.1 August 2026)components with native async: async func, stream<T>, future<T>; wasi:io removedcurrent; Wasmtime 46+ implements 0.3.0; 0.3.x continues on the release train
WASI 1.0not scheduled"may be scheduled outside the train" per the roadmap
  • Preview 1 is a flat set of C-like functions (fd_write, path_open, args_get, clock_time_get, random_get) taking i32 pointers. It works with any core module but cannot express rich types.
  • 0.2 moved WASI onto the component model: interfaces are typed in WIT and grouped into worlds such as wasi:cli/command (a CLI program) and wasi:http/proxy (an HTTP handler).
  • 0.3 is a mechanical migration from 0.2: pollable becomes future<T>, input-stream becomes stream<u8>, and start-x / finish-x pairs become one async func. The HTTP handler world is wasi:http/service in 0.3 tooling. Many hosts and guest toolchains still default to 0.2, so check both ends before switching.

Capability-based security

 host (wasmtime, node, ...)                guest.wasm
 ──────────────────────────                ──────────
 --dir ./data::/data   ── preopen fd 3 ──►  open("/data/x")   ok
                                            open("/etc/passwd") ENOTCAPABLE
 --env KEY=v           ── environ ──────►  getenv("KEY")
 (no -S inherit-network)                   connect(...)       denied
 stdin / stdout / stderr inherited         printf(...)        ok
 wall clock, random                        time(), rand()     ok
PrincipleMeaning
No ambient authoritya module starts with nothing: no files, env, network or clock unless the host grants it
Preopened directoriesthe host opens directories and passes handles; paths resolve only beneath them (no .. escape)
Handles, not namescomponents receive resources (descriptor, tcp-socket) and can only use what they were given
Deny by defaultnetwork, env vars and extra files are opt-in flags in every serious runtime
Virtualizationa component can import an interface implemented by another component (a fake FS, a logger)

Node's node:wasi exposes the same options but explicitly does not provide a secure sandbox (see its docs); use Wasmtime or another runtime for untrusted code.

Runtimes

RuntimeKindWASI support and notes
WasmtimeBytecode Alliance, Rust, Cranelift JIT / AOTreference host for components, WASI 0.2 and 0.3, wasi:http; monthly major releases (49.x in September 2026)
WasmerRust, several backendspreview 1 plus its own WASIX POSIX extensions; package registry at wasmer.io
WasmEdgeCNCF, C++preview 1, networking and wasi-nn for AI inference; edge and cloud-native
WAMRBytecode Alliance micro runtime, Cinterpreter, AOT and JIT for embedded and IoT; preview 1
Node.js node:wasibuilt inpreview 1 only; Stability: 1 - Experimental; not a security boundary
Bunbuilt inpartial node:wasi (args, env, preopens, start); bun ./app.wasm runs a WASI command
Denobuilt innode:wasi is a non-functional stub; use jco output or a userland shim
Browsersvia JSno native WASI: jco transpile + @bytecodealliance/preview2-shim, or preview 1 shims
Cloudflare WorkersV8 isolatescore Wasm through the JS API (e.g. Rust via workers-rs); not a WASI 0.2 host
Fastly ComputeWasmtime-based edgeWasm guests via Rust, JS and Go SDKs
Spin (CNCF)framework on Wasmtimecomponents and wasi:http; spin new, spin build, spin up
wasmCloud (CNCF)distributed platformcomponents linked across hosts; WIT-first

Wasmtime CLI

curl https://wasmtime.dev/install.sh -sSf | bash
wasmtime --version
 
wasmtime run app.wasm arg1 arg2        # "run" is default
wasmtime --dir ./data::/data app.wasm  # preopen
wasmtime --env API=x --env HOME app.wasm
wasmtime run --invoke 'add(1, 2)' comp.wasm
wasmtime serve -S cli --addr 0.0.0.0:8080 http.wasm
wasmtime compile app.wasm -o app.cwasm # AOT
FlagEffect
--dir HOST[::GUEST]preopen a host directory, optionally under another guest path
--env NAME=VAL / --env NAMEset a variable / inherit it from the host
-S inherit-envinherit every host environment variable
-S inherit-networkfull network access for wasi:sockets
-S tcp, -S udp, -S allow-ip-name-lookupfiner-grained socket switches
-S httpprovide wasi:http outgoing requests to run
-S cliinclude the wasi:cli imports (used with serve)
-S p3enable the WASIp3 (0.3) host APIs where not on by default
-S help, -W help, -O help, -C helplist WASI, Wasm-feature, optimization and codegen options
-W gc, -W component-model-asynctoggle Wasm proposals
--invoke namecall an export instead of _start / wasi:cli/run; components take WAVE syntax 'f("x", 1)'
-W fuel=N, -W max-memory-size=NCPU (instruction fuel) and memory limits

All Wasmtime flags go before the .wasm file; everything after it is passed to the guest as arguments. wasmtime serve runs a wasi:http component as a local HTTP server and is for development only.

WIT basics

wit/world.wit
package acme:shapes@0.1.0;
 
interface types {
  record point { x: f32, y: f32 }
 
  variant shape {
    circle(f32),
    rect(tuple<f32, f32>),
    empty,
  }
 
  enum unit { px, mm, inch }
 
  flags style { bold, italic, underline }
 
  type points = list<point>;
}
 
interface geometry {
  use types.{point, shape};
 
  area: func(s: shape) -> f32;
  centroid: func(pts: list<point>) -> option<point>;
  parse: func(src: string) -> result<shape, string>;
 
  resource canvas {
    constructor(width: u32, height: u32);
    draw: func(s: shape);
    pixels: func() -> list<u8>;
    blank: static func() -> canvas;
  }
}
 
world shapes {
  import log: func(msg: string);
  export geometry;
}
WITMeaningJS / TS (jco)Rust (wit-bindgen)
bool, s8..s64, u8..u64integersnumber (bigint for 64-bit)i8..u64
f32, f64, char, stringscalars, Unicodenumber, stringf32, f64, char, String
list<T>sequenceT[] (Uint8Array for list<u8>)Vec<T>
option<T>maybeT | undefinedOption<T>
result<T, E>success or errorreturns T, throws ComponentError with EResult<T, E>
tuple<A, B>fixed tuple[A, B](A, B)
recordstruct with named fieldsobjectstruct
varianttagged union with payloads{ tag: "circle", val: 1.5 }enum with data
enumtag onlystring literal union ("px")C-like enum
flagsbit setobject of booleansbitflags struct
resourcehandle to an object owned by one sideclasstrait + handle type
own<T> / borrow<T>ownership of a resource handleowned / &
future<T>, stream<T>async values (WASI 0.3)promise / stream-likeasync types
type x = ...alias
ItemSyntax
Packagepackage ns:name@1.2.3; first in the file (or a package ... { } block)
Interfaceinterface name { ... }: types and functions
Worldworld name { import ...; export ...; }: the full contract of one component
Useuse types.{point}; in an interface; use wasi:io/streams@0.2.0.{input-stream}; across packages
Includeinclude wasi:cli/imports@0.2.0; merges another world
Functionsname: func(a: u32) -> string;; async func in WASI 0.3
Identifierskebab-case (get-user); % escapes keywords (%type)
Gates@since(version = 0.2.0), @unstable(feature = x), @deprecated(...)
Dependencieswit/deps/<pkg>/*.wit, fetched with wkg fetch

Canonical ABI

 component (shapes.wasm)
 ┌──────────────────────────────────────────────┐
 │  WIT world: import log, export geometry      │
 │   ┌──────────────┐  canon lower   ┌────────┐ │
 │   │ core module  │◄───────────────┤ import │ │
 │   │ (Rust, C...) │  canon lift    ├────────┤ │
 │   │ own memory,  ├───────────────►│ export │ │
 │   │ cabi_realloc │                └────────┘ │
 │   └──────────────┘                           │
 └──────────────────────────────────────────────┘
       ▲ strings, lists, records are copied and
       │ re-encoded at the boundary; no shared memory
  • A component wraps one or more core modules plus type information. Components never share linear memory: values are copied between them.
  • Lifting turns core values (pointers, lengths, flat i32s) into WIT values; lowering does the reverse. The core module exports cabi_realloc so the host can allocate space for incoming strings and lists.
  • cabi_post_* exports let the callee free return buffers after the caller has copied them.
  • Resources are i32 handles into per-component tables; borrow handles are only valid for the call.
  • You rarely see any of this: bindings generators (wit-bindgen, jco, componentize-py) write both sides. It matters when debugging traps inside generated glue or measuring copy costs.

Tooling

ToolDoes
wasm-tools component new core.wasm -o c.wasmwrap a core module (with embedded WIT) into a component; --adapt a preview 1 adapter
wasm-tools component embed wit/ core.wasm -o e.wasmembed WIT type info into a core module
wasm-tools component wit c.wasmprint a component's WIT (components are self-describing)
wasm-tools print c.wasm / validateinspect and validate components too
wit-bindgen rust, c, csharp, ...guest bindings; in Rust usually the wit_bindgen::generate! macro
Rust wasm32-wasip2 targetcargo build emits a component directly (no extra tool)
cargo-componentolder Cargo subcommand; being deprecated in favor of the native target + wit-bindgen
jco transpile c.wasm -o out/component → ES module + core .wasm + .d.ts, for Node and browsers
jco componentize app.js -w wit -o c.wasmJS / TS → component (ComponentizeJS on StarlingMonkey)
jco types wit/, jco guest-types wit/TS declarations for hosts / for JS guests
jco run, jco serverun a command / HTTP component in Node (serve is dev-only)
componentize-py -d wit -w world componentize app -o c.wasmPython → component (CPython inside)
TinyGo -target=wasip2, componentize-goGo → component
wac plug, wac composecompose components by wiring exports to imports
wkg fetch, wkg get, wkg publish, wkg oci pullfetch WIT dependencies; get, publish and pull packages and components (OCI)
cargo install --locked wasm-tools wac-cli wkg
npm i -D @bytecodealliance/jco
pip install componentize-py

Composition and registries

# app.wasm imports acme:kv/store; redis.wasm exports it
wac plug app.wasm --plug redis.wasm -o composed.wasm
wasm-tools component wit composed.wasm   # fewer imports
 
# packages and components live in OCI registries
wkg get wasi:http@0.2.1          # a WIT package
wkg fetch                        # deps for ./wit
wkg publish composed.wasm        # configured registry
wkg oci pull ghcr.io/webassembly/wasi/http:0.2.1
  • Composition links components at build time into one component; the result still only talks through WIT, so each part keeps its own memory and sandbox.
  • wac compose takes a small .wac script for complex graphs; wac plug covers the common "fill these imports" case.
  • Components are distributed as OCI artifacts (the same registries as containers), and WIT packages are resolved by wkg from registry namespaces such as wasi: and ba:.

Core module or component?

Choose a core module whenChoose a component when
the host is a browser page and you own the JS gluethe host is Wasmtime, Spin, wasmCloud or another component runtime
you need shared memory, threads or SIMD-heavy kernels with zero-copy viewsyou want typed, language-neutral interfaces (WIT) instead of pointer contracts
wasm-bindgen / Emscripten already give you good TypeScript bindingsyou are building plugins that third parties write in other languages
smallest possible download and startup matteryou want to compose parts, virtualize capabilities, or target wasi:http
you need preview 1 runtimes (Node node:wasi, Wasmer, WAMR)you are writing new server-side or edge code in 2026

Components run in browsers only through jco transpile (JS glue over core Wasm); browsers have no native component model support and it is still a phase-1 proposal at the W3C CG.

Recipes

Rust CLI for WASI

Use for a portable command-line tool that runs on any WASI host.

src/main.rs
use std::{env, fs, io::Write};
 
fn main() -> std::io::Result<()> {
    let path = env::args().nth(1).expect("usage: wc <file>");
    let text = fs::read_to_string(&path)?;
    let words = text.split_whitespace().count();
    let who = env::var("USER").unwrap_or("?".into());
    writeln!(std::io::stdout(), "{who}: {words} words")?;
    Ok(())
}
rustup target add wasm32-wasip1 wasm32-wasip2
cargo build --release --target wasm32-wasip2 # component
cargo build --release --target wasm32-wasip1 # core

Run with a preopened directory

Use to give a guest exactly one directory and nothing else.

W=target/wasm32-wasip2/release/wc.wasm
wasmtime run --dir ./notes::/notes --env USER=ada \
  $W /notes/todo.md
# ada: 42 words
 
wasmtime run $W /etc/passwd   # no preopen for /etc
# Error: Os { ..., kind: NotFound, ... }

WIT world implemented in Rust

Use to build a reusable library component with a typed interface.

wit/world.wit
package acme:calc@0.1.0;
 
interface ops {
  record stats { min: f64, max: f64, mean: f64 }
  summarize: func(xs: list<f64>) -> result<stats, string>;
}
 
world calc {
  export ops;
}
Cargo.toml
[lib]
crate-type = ["cdylib"]
 
[dependencies]
wit-bindgen = "0.62"
src/lib.rs
mod bindings {
    wit_bindgen::generate!({ path: "wit" });
    use super::Calc;
    export!(Calc);
}
use bindings::exports::acme::calc::ops::{Guest, Stats};
 
struct Calc;
 
impl Guest for Calc {
    fn summarize(xs: Vec<f64>) -> Result<Stats, String> {
        if xs.is_empty() {
            return Err("empty input".into());
        }
        let it = xs.iter().copied();
        let min = it.clone().fold(f64::MAX, f64::min);
        let max = it.fold(f64::MIN, f64::max);
        let mean = xs.iter().sum::<f64>() / xs.len() as f64;
        Ok(Stats { min, max, mean })
    }
}
cargo build --release --target wasm32-wasip2
wasm-tools component wit \
  target/wasm32-wasip2/release/calc.wasm
wasmtime run --invoke 'summarize([1.0, 5.0])' \
  target/wasm32-wasip2/release/calc.wasm

Component in Node or browser

Use jco to turn any component into an ES module with TypeScript types.

npx jco transpile calc.wasm -o src/calc --name calc
# src/calc/calc.js, calc.core.wasm, calc.d.ts,
# interfaces/acme-calc-ops.d.ts
import { ops } from "./calc/calc.js";
 
try {
  const s = ops.summarize(new Float64Array([1, 5, 9]));
  console.log(s.mean); // 5
} catch (e) {
  // result<_, string> error arrives as a thrown
  // ComponentError whose payload is the string
  console.error((e as { payload?: unknown }).payload);
}

The output imports WASI from @bytecodealliance/preview2-shim when the component needs it (install it next to the output); bundle it with Vite or esbuild for the browser. Use --instantiation async to pass imports yourself instead of ES imports.

Preview 1 module in Node

Use for wasm32-wasip1 / wasi-sdk binaries without a separate runtime.

import { readFile } from "node:fs/promises";
import { WASI } from "node:wasi";
import { argv, env } from "node:process";
 
const wasi = new WASI({
  version: "preview1",          // required
  args: ["wc", "/notes/todo.md"], // argv[0] first
  env: { USER: env.USER ?? "" },
  preopens: { "/notes": "./notes" }, // guest: host
});
const mod = await WebAssembly.compile(
  await readFile("./wc.wasm"),
);
const inst = await WebAssembly.instantiate(
  mod, wasi.getImportObject(),
);
const code = wasi.start(inst); // exit code
console.log("exit", code, argv.length);

wasi.start is for commands (_start); call wasi.initialize(instance) for reactor modules that export _initialize. Remember that node:wasi is experimental and not a sandbox.

Componentize JavaScript

Use to write a component in TypeScript/JS, for example a plugin for a Wasmtime-based host.

wit/world.wit
package acme:greet@0.1.0;
 
world greeter {
  export greet: func(name: string) -> string;
}
greeter.ts
export function greet(name: string): string {
  return `Hello, ${name}!`;
}
npx jco componentize greeter.ts -w wit -o greeter.wasm
npx jco wit greeter.wasm
wasmtime run --invoke 'greet("Ada")' greeter.wasm

The output embeds a JS engine (StarlingMonkey), so expect multi-megabyte components; jco componentize --backend qjs selects a QuickJS-based backend instead.

References