Browser-based, Node-compatible runtime + WASI runner — a WebContainers-like system
built from scratch. Run Express, npm install, a dev server, even WASI binaries,
inside a browser tab. Pet project; the goal is deep understanding of how these
systems work, plus a practical "Express + npm install in the browser".
Status: active milestone M10 (Real Tooling). Pet project — APIs are
0.xand may move. SeePROJECT_PLAN.md,TASKS.md,docs/adr/,docs/compat/.
Want everything in one install? npm i @riftydev/sdk — the umbrella front door
(packages/rifty): a framework-free createSandbox() plus every
layer below on a subpath (@riftydev/sdk/vfs, @riftydev/sdk/runtime, @riftydev/sdk/net, …). Or take just
the part you need: every layer is also its own package. All are ESM, ship .d.ts, and
are released in lockstep under the @riftydev scope.
| Package | What it is | Runs in |
|---|---|---|
@riftydev/sdk |
Umbrella: one-install front door + createSandbox() |
browser + Worker |
@riftydev/io |
EventEmitter, Buffer, node-compatible streams | anywhere |
@riftydev/vfs |
Virtual FS: in-memory + OPFS, with a sync mirror | anywhere |
@riftydev/kernel |
Processes / scheduling / IPC (Worker-as-process, SAB) | browser + Worker |
@riftydev/net |
node:net/http/https/ws + node:sqlite (sql.js) |
anywhere |
@riftydev/runtime-js |
Node-compatible JS runtime: CJS/ESM loader + builtins | browser + Worker |
@riftydev/runtime-wasi |
WASI (preview1) runner for .wasm guests |
browser + Worker |
@riftydev/npm-client |
In-browser npm: semver, registry, unpack, link, install | anywhere |
@riftydev/shell |
Tiny bash-flavoured shell over @riftydev/vfs |
anywhere |
@riftydev/terminal |
xterm.js terminal wrapper | browser |
@riftydev/service-worker |
Service Worker preview/HMR routing bridge | browser |
@riftydev/shadow-registry |
Data tables of in-browser npm substitutions | anywhere |
npm install @riftydev/sdk # everything + createSandbox() (the front door)
npm install @riftydev/vfs # just the VFS
npm install @riftydev/npm-client # just the npm resolver/installer
# …or any combination — they share singletons when installed at the same versionimport { MemoryVfs, joinPath } from '@riftydev/vfs';
const vfs = new MemoryVfs();
await vfs.mkdir('/proj', { recursive: true });
await vfs.writeFile('/proj/hello.txt', 'hi from rifty');
console.log(await vfs.readFileText(joinPath('/proj', 'hello.txt'))); // "hi from rifty"import { parse, matchesRange, pickBestVersion } from '@riftydev/npm-client';
matchesRange('1.4.2', '^1.2.0'); // true
pickBestVersion(['1.0.0', '1.4.2', '2.0.0'], '^1.2.0'); // "1.4.2"More, runnable, in examples/standalone-usage
(pnpm --filter @rifty-examples/standalone start). The full product demo is the
playground (pnpm dev).
The leaf packages (@riftydev/io, @riftydev/vfs, @riftydev/npm-client, @riftydev/shell,
@riftydev/shadow-registry) are plain isomorphic JS and need nothing special. But the
runtime (runtime-js, runtime-wasi, kernel, service-worker) has hard
browser prerequisites — without them it will not boot:
-
Cross-origin isolation is mandatory (for
SharedArrayBuffer+Atomics.wait, used by synchronous IPC and sync fs). Serve your page with:Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: credentialless Cross-Origin-Resource-Policy: cross-originThen
globalThis.crossOriginIsolated === true. Header-less static hosts (e.g. GitHub Pages) do not work; Vercel / Netlify / Cloudflare Pages do. Copy-paste configs:vercel.json,apps/playground/public/_headers, and the dev-serverheadersinapps/playground/vite.config.ts. -
A bundler with module Workers +
new URL('…', import.meta.url)worker resolution (Viteworker: { format: 'es' }).runtime-js/runtime-wasispawn their worker entry by URL (@riftydev/runtime-js/worker,@riftydev/runtime-wasi/worker-entry). -
A service worker for preview/HMR routing — build one from
@riftydev/service-worker/swand register it viaregisterServiceWorker(url). There is no prebuiltsw.js; it must be bundled (the playground does this withapps/playground/build/sw-plugin.ts). -
Same-origin WASM assets when you use them:
node:sqliteneedssql.js/dist/sql-wasm.wasmreachable (inject alocateFileviainitSqliteEngine({ locateFile }), awaited once before anyDatabaseSync); the real-tooling WASI path needs itsesbuild.wasm.
Given those, the umbrella's createSandbox() does the rest of the boot wiring
(capability probe → COI guard → VFS backend with memory fallback → service-worker
registration → runtime worker) and hands you a live RuntimeController:
import { checkCapabilities, createSandbox } from '@riftydev/sdk';
if (!checkCapabilities().sufficient) return showUnsupportedNotice();
const sandbox = await createSandbox({
workerUrl: new URL('@riftydev/runtime-js/worker', import.meta.url), // your bundler resolves it
serviceWorkerUrl: '/sw.js',
});
await sandbox.runtime.eval('console.log("hello from a Worker")');Target es2022; Chrome-first (cross-browser e2e infra exists, see
docs/compat/).
pnpm install
pnpm dev # playground at http://localhost:5273
pnpm typecheck # workspace-wide
pnpm lint # biome
pnpm build:libs # build all publishable packages to dist/
pnpm test:run # unit
pnpm test:parity # node parity runner
pnpm test:e2e # playwright (chromium)The in-repo exports point at raw TypeScript src/ (so dev/HMR needs no build);
the published packages point at the built dist/ via publishConfig. See
docs/PUBLISHING.md and ADR-0070.
Five layers, top-down only — no reverse imports:
apps/playground (UI: Monaco editor + xterm terminal — SolidJS, isolated)
shell · terminal · npm-client
runtime-js (Node API) · runtime-wasi (WASI)
kernel (processes, scheduling, IPC)
vfs · io · net (+ service-worker, shadow-registry)
UI framework (SolidJS) is confined to apps/playground/** (D-002).
The rules live in CLAUDE.md: TDD (tests/parity-case first), no any,
no silent stubs (throw NotImplementedError), one change per PR. Decisions are
recorded as ADRs.
MIT.