How Nodepod works
Nodepod recreates useful Node.js development primitives with browser-native building blocks. It is not a port of the Node.js executable and it does not boot a virtual machine. The runtime coordinates JavaScript execution, a virtual filesystem, package transforms, worker-backed processes, and virtual networking inside the host environment.
Start at the SDK
Section titled “Start at the SDK”The browser entry exports Nodepod from src/index.ts. A host application creates an instance and owns its lifecycle:
import { Nodepod } from '@scelar/nodepod';
const pod = await Nodepod.boot({ files: { '/home/project/index.js': 'console.log("hello")', }, workdir: '/home/project',});
const process = await pod.spawn('node', ['index.js']);await process.completion;
pod.teardown();src/sdk/nodepod.ts is the coordinator. It initializes the engine and host adapters, exposes public subsystems, tracks workers and virtual servers, and releases them during teardown. src/sdk/nodepod-fs.ts, src/sdk/nodepod-process.ts, and src/sdk/nodepod-terminal.ts provide the focused filesystem, process, and terminal surfaces.
Runtime map
Section titled “Runtime map”Host application | vNodepod SDK and lifecycle | +----------+-----------+------------+ | | | | v v v vVirtual FS Packages Processes Virtual HTTP | | | | +----------+----- worker/host -------+ adaptersThe boundaries are deliberate:
- The SDK owns public lifecycle and orchestration.
- The host layer adapts browser workers, Node.js worker threads, storage, and HTTP ingress.
- The engine evaluates modules against Node-compatible globals and polyfills.
- The virtual filesystem stores project state and synchronizes the data processes need.
- The package system resolves registries, extracts archives, and transforms modules.
- The request proxy connects virtual listening ports to programmatic requests and browser previews.
Virtual filesystem
Section titled “Virtual filesystem”src/memory-volume.ts implements the in-memory POSIX-style tree used by the SDK and polyfills. Files and directories carry metadata, and mutation events support watchers and worker synchronization.
The fs polyfill in src/polyfills/fs.ts adapts that volume to common Node.js call shapes. SDK calls such as pod.fs.writeFile() use the same underlying project state that code inside a spawned process sees.
Snapshots serialize this filesystem boundary. A shallow snapshot can omit reinstallable package data, while a full snapshot carries more of the current tree. Snapshots are application data, not a security boundary.
Module execution and Node compatibility
Section titled “Module execution and Node compatibility”src/script-engine.ts resolves and executes modules. It provides CommonJS wrapping, module caching, package export resolution, JSON and Wasm handling, and Node-shaped globals such as process, Buffer, and timers.
Modern packages frequently ship ESM syntax even when a consumer needs CommonJS-like execution. src/syntax-transforms.ts and src/module-transformer.ts handle syntax conversion and package-level transforms. Nodepod can initialize esbuild-wasm only when a workflow needs it or preload it during boot.
Polyfills under src/polyfills/ implement compatible portions of Node’s built-in modules. Compatibility is practical rather than absolute: a package can still depend on native add-ons, kernel behaviour, binaries, or unimplemented edges that a browser cannot provide.
Packages
Section titled “Packages”The package pipeline lives under src/packages/. It resolves versions and exports, downloads package archives, extracts them into the virtual filesystem, and coordinates transformations.
Package content may be cached in browser storage across boots. Runtime project state still belongs to the Nodepod instance, and package fetches remain subject to browser CORS. When you configure Nodepod’s optional CORS proxy, allowedFetchDomains controls its destination allowlist; setting it to null disables that check and should be intentional.
Processes, workers, and shared state
Section titled “Processes, workers, and shared state”The process system under src/threading/ maps spawned commands to Web Workers in the browser. src/threading/process-manager.ts coordinates process creation, output, input, signals, resize events, virtual server messages, and completion. src/threading/process-handle.ts exposes that lifecycle to the SDK.
Workers begin with a filesystem snapshot and receive changes through the VFS bridge. When cross-origin isolation makes SharedArrayBuffer available, Nodepod can use shared filesystem data and atomics for synchronous cross-worker operations. Without it, the runtime uses asynchronous message passing and full snapshots where possible. APIs that fundamentally require synchronous cross-worker behaviour throw rather than pretending to be reliable.
The same process protocol can run through Node.js worker threads. The host abstraction in src/host/ selects browser or Node implementations without requiring the public SDK to fork into unrelated designs.
Shell and terminal
Section titled “Shell and terminal”src/shell/ parses shell input and implements built-ins against the virtual filesystem and process manager. A Nodepod terminal is a UI adapter around that shell; xterm.js is passed in by the host application and is not embedded into the runtime package.
createTerminal() maintains a persistent shell worker so commands share the current directory, environment, and filesystem state. Raw spawn() output stays unchanged. Terminal-owned output can visually rewrite loopback server URLs to the actual preview URL through rewriteTerminalUrls.
Virtual HTTP and previews
Section titled “Virtual HTTP and previews”Node-compatible servers register listening ports in the runtime. Application code can call a server directly with pod.request() or pod.proxy.handleRequest() without any service worker. This is the portable path for headless tests and reduced static-host deployments.
Navigable previews add two browser layers:
src/request-proxy.tsroutes browser requests and server events.- The service worker and preview bridge connect a browser navigation to the correct Nodepod instance and virtual port.
On local loopback hosts, previewOrigin: 'auto' can use isolated localhost subdomains. In production, a wildcard-domain template such as https://{instanceId}-{port}.preview.example.com gives each preview a dedicated origin. The preview hostname must route to the same deployment so it can load the bootstrap bridge before control transfers to the virtual server.
Setting previewOrigin: false retains path-based URLs. Setting serviceWorker: false disables navigable previews entirely but leaves programmatic requests available.
Headless execution
Section titled “Headless execution”Headless mode is the same core runtime with UI-oriented defaults turned off:
// Browser: Web Workers, service worker off by default.import { Nodepod } from '@scelar/nodepod';const browserPod = await Nodepod.boot({ headless: true });
// Node.js or Bun: worker threads and loopback HTTP ingress.import { Nodepod as HeadlessNodepod } from '@scelar/nodepod/headless';const hostPod = await HeadlessNodepod.boot();src/headless.ts selects headless defaults and host adapters. In Node.js or Bun, pod.port() can return a loopback ingress URL. In the browser, pod.request() is the direct way to reach a virtual server when no preview service worker exists.
Profiling and inspection
Section titled “Profiling and inspection”Profiling code under src/profiling/ can collect runtime spans, counters, timings, memory samples, long-task samples, and trace exports. It is disabled by default to avoid paying diagnostic overhead in every session.
Preview inspection in src/sdk/preview-inspector.ts uses the preview bridge to query an attached document, console output, errors, navigation, accessibility summaries, interactions, and best-effort screenshots. It follows the browser messaging and origin model; it is not a mechanism for inspecting arbitrary pages.
Security boundary
Section titled “Security boundary”Workers, preview origins, and browser storage are useful isolation mechanisms, but Nodepod is not a hardened sandbox. A product embedding adversarial code must enforce its own limits and should use a purpose-built server-side isolation system when its threat model requires stronger guarantees.
Continue with Security model and Compatibility and browser requirements before deploying user-controlled runtimes.