@gadgetinc/substrate
v0.1.0-rc.9
Published
Browser-native JavaScript runtime with Node.js compatibility, powered by a Rust/WASM kernel.
Keywords
Readme
Substrate
Browser-native JavaScript runtime with Node.js compatibility, powered by a Rust/WASM kernel.
Substrate runs Node-style code entirely in the browser. It provides a virtual filesystem, process model, and 39 Node.js builtin modules so that tools like Vite, npm, and pnpm work without a server.
Install
pnpm add @gadgetinc/substrateQuick Start
import { bootSubstrate } from "@gadgetinc/substrate";
const substrate = await bootSubstrate();
await substrate.mount({
"server.js": {
file: {
contents: `
const http = require("http");
http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello from Substrate!");
}).listen(8080);
`,
},
},
});
const proc = substrate.spawn("node", ["server.js"]);
await substrate.waitForPort(8080);
const response = await fetch("http://localhost:8080/");
console.log(await response.text()); // "Hello from Substrate!"Boot Configuration
bootSubstrate accepts an optional config object:
const substrate = await bootSubstrate({
// Multi-tab coordination — all tabs with the same name share a kernel
name: "my-app",
// Initial working directory (default: "/")
cwd: "/project",
// Environment variables
env: { NODE_ENV: "development" },
// Package managers to install (npm enabled by default, others opt-in)
binaries: {
pnpm: true,
yarn: true,
},
// CORS proxy for external requests
proxyUrl: "https://cors-proxy.example.com",
neverProxy: ["registry.npmjs.org"],
// Persist the virtual filesystem across page reloads via OPFS
persistence: { storeName: "my-project" },
// Runtime logging
logging: { level: "info" },
// Callbacks
onConsole: (level, args) => console.log(`[${level}]`, ...args),
onPortListen: (port) => console.log(`Server listening on :${port}`),
onPortClose: (port) => console.log(`Server closed on :${port}`),
});Mounting Files
mount() writes a file tree to the virtual filesystem. You can mount source code, node_modules, config files — anything the guest process needs.
await substrate.mount({
"package.json": {
file: { contents: JSON.stringify({ name: "my-app", type: "module" }) },
},
src: {
directory: {
"index.js": {
file: { contents: `console.log("hello")` },
},
},
},
});Mount accepts an optional second argument for the mount point (default "/"):
await substrate.mount(tree, "/project");Spawning Processes
const proc = substrate.spawn("node", ["src/index.js"], {
cwd: "/project",
env: { DEBUG: "true" },
});
// Stream stdout/stderr
const reader = proc.stdout.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(value);
}
// Wait for exit
const exitCode = await proc.exit;
// Or kill the process
proc.kill();Package manager commands work the same way:
const install = substrate.spawn("pnpm", ["install"]);
await install.exit;
const dev = substrate.spawn("pnpm", ["run", "dev"]);
await substrate.waitForPort(5173);Virtual Networking
Servers started inside Substrate are accessible via fetch() from the host page. The Service Worker intercepts requests to localhost and routes them to the virtual kernel.
substrate.spawn("node", ["server.js"]); // Listens on :3000
await substrate.waitForPort(3000);
// fetch() just works — the Service Worker handles routing
const res = await fetch("http://localhost:3000/api/data");WebSocket connections are also routed through the virtual network automatically:
const ws = new WebSocket("ws://localhost:3000");
ws.onopen = () => ws.send("hello");
ws.onmessage = (e) => console.log(e.data);Base Path Routing
For embedding in iframes or serving from a subpath:
const substrate = await bootSubstrate({
basePath: "/__preview__",
basePathPort: 5173,
});
// Navigating to /__preview__/ routes to the virtual server on :5173Filesystem Operations
Read and write files directly without going through a process:
await substrate.writeFile("/project/config.json", JSON.stringify(config));
const contents = await substrate.readFile("/project/config.json");Persistence
Persist the virtual filesystem to OPFS so it survives page reloads:
// Enable at boot
const substrate = await bootSubstrate({
persistence: { storeName: "my-project" },
});
// ... mount files, run processes ...
// Save current state
await substrate.persist();
// On next page load, the VFS is restored automatically from the storeYou can also manage stores after boot:
await substrate.mountStore("another-project");
const stores = await substrate.listStores();
await substrate.deleteStore("old-project");WASM Package Interception
When native Node.js addons are detected during mount(), Substrate automatically fetches WASM alternatives from npm. Built-in support is included for rollup, esbuild, and rolldown.
You can register additional mappings:
const substrate = await bootSubstrate({
wasmPackages: [
{
packageName: "my-native-tool",
wasmPackage: "my-native-tool-wasm",
nativeBindingPatterns: ["^@my-native-tool/binding-"],
wasmExportPath: "dist/index.js",
},
],
});Multi-Tab Coordination
When multiple tabs boot Substrate with the same name, one tab becomes the leader (owns the kernel) and others become followers. Leadership changes are communicated via events:
const substrate = await bootSubstrate({ name: "my-app" });
console.log(substrate.isLeader); // true or false
substrate.on("leadership", (isLeader) => {
console.log(isLeader ? "Became leader" : "Lost leadership");
});Limitations
See docs/limitations.md for a full list. Key points:
- Native Node.js addons (
.nodefiles) don't work unless a WASM alternative is available cryptosupports hashing and HMAC but not ciphers, signing, or key exchangedns,tls,http2are stubbed — the browser handles these natively- Brotli compression is not supported (deflate/gzip work)
- Integration tests run against Chromium; Firefox and Safari are untested
Development
Requires Nix (via direnv) for the development environment.
pnpm build # Build kernel + JS bundles
pnpm test # Unit tests (vitest)
pnpm test:integration # Integration tests (Playwright)
pnpm typecheck # Type check (tsgo)
pnpm lint # Lint (oxlint + oxfmt)
pnpm dev # Dev server (Vite playground)Repository structure
crates/kernel/ — Rust WASM kernel (filesystem, process table, syscalls)
packages/substrate/ — TypeScript runtime, node compat, boot API
packages/playground/ — Dev playgrounds (Vite, Express, Fastify, React Router)
scripts/ — Build and release tooling
docs/architecture/ — Architecture deep dives