Running code in the browser is one of those things that sounds simple until you actually try to build it. For years, the only real option was WebContainers by StackBlitz - a proprietary tool with no public pricing that locks you into an enterprise sales process just to find out what it costs.
We needed this exact capability for Scelar. We couldn't justify that price. So we built our own and made it completely free and open source.
Meet Nodepod.
Quick links
- GitHub - Nodepod - the runtime itself (MIT licensed)
- GitHub - wZed - browser-native IDE built on Nodepod
- npm - nodepod -
npm install @scelar/nodepod
Contents
- The road to getting here
- First, what problem does this solve?
- The WebContainers problem
- What Nodepod actually is
- The systems under the hood
- How it compares
- What works today
- What it can't do (and why that's okay)
- See it in action: wZed
- Why we made it open source
The road to getting here
Nodepod has been in the making for about a month - and it hasn't been a straight line. We went through roughly 10 different iterations, each one starting over from scratch because the previous approach fell apart in some fundamental way.
Here's the honest version of how it went:
Attempt 1-2: Editing the Node.js source code and compiling to WebAssembly. The idea was simple - take the actual Node.js codebase, strip out the parts we don't need, and compile it to WASM so it runs in the browser. Turns out, this is an enormous task. Node.js is hundreds of thousands of lines of C++ tightly coupled to V8 and libuv. The resulting WASM binary would have been massive, the build pipeline was a nightmare, and we realized we'd basically be maintaining a fork of Node.js forever. Abandoned.
Attempt 3-4: Expanding QuickJS with polyfills. QuickJS is a tiny JavaScript engine that compiles to WASM easily. The plan was to use it as the execution engine and bolt on all the Node.js APIs as polyfills. But QuickJS is intentionally minimal - it lacks the infrastructure needed to support the full module resolution system, proper async/await integration, and the sheer breadth of Node.js built-ins we needed. Every polyfill we tried to add exposed another limitation. Too constrained.
Attempt 5-7: Various other approaches - different WASM-based engines, hybrid server/client architectures, modified browser APIs. Each one hit a wall. Either too slow, too heavy, too fragile, or too limited.
Attempt 8-10: Back to the beginning - rewrite Node.js from the ground up in TypeScript. This was the breakthrough. No WASM, no porting C++ - just pure JavaScript polyfills, an in-memory filesystem, and a custom execution engine that uses the browser's own JS engine to run code. The hard part wasn't the idea though - it was the sync/async problem. In real Node.js, readFileSync blocks the thread and returns immediately. In the browser, nothing is truly synchronous. The entire execution model fights you.
We had to build a custom Promise system that could resolve synchronously when the data was already available, and use SharedArrayBuffer with Atomics.wait() for cases where one process genuinely needs to block and wait for another. Getting that right - along with the Web Worker process model - took almost as long as writing the rest of the runtime.
The result was worth it. But it was a grind.
Note: Nodepod is live on GitHub - check it out at ScelarOrg/NodePod. We also built wZed, a browser-native code editor (inspired by Zed) that uses Nodepod as its runtime - it's the easiest way to see Nodepod in action.
First, what problem does this solve?
Let's back up. Normally, when you write code (say, a website or an app), you need to run it somewhere - a computer, a server, the cloud. Your browser can display websites, but it can't run the code that builds them. It doesn't know how to install packages, start servers, or execute shell commands.
This is a problem if you want to build products like:
- Code playgrounds - where people can write and run code without installing anything
- Live documentation - where code examples actually work, not just sit there
- AI coding tools - where generated code needs to be previewed instantly
- Educational platforms - where students learn by doing, right in the browser
The traditional solution is to spin up a server for every user. That's expensive, slow, and doesn't scale. The modern solution is to somehow make the browser itself act like a computer. That's what Nodepod does.
The WebContainers problem
Until now, WebContainers (by StackBlitz) was basically the only game in town. It works - but it comes with serious baggage:
- Opaque, enterprise-only pricing - there's no public pricing page. We tried to contact StackBlitz multiple times about commercial licensing and never got a response. A while back we found a Reddit thread where someone claimed they did get a response and were quoted $27,000 per year. We were never able to verify that number, and we couldn't find the post again later. What we do know is that it's not cheap and it's not transparent
- Not fully open source - parts are proprietary, which means you're locked into their ecosystem
- Heavy - the core is a multi-megabyte WebAssembly binary that takes 2-5 seconds to boot
- Vendor lock-in - if they raise prices or change terms, you have no alternative. And good luck getting them on the phone
For a startup like ours, paying potentially tens of thousands of dollars per year for a dependency - before we've even charged a customer - was a non-starter. The fact that we couldn't even get a straight answer on what it would cost made it worse.
What Nodepod actually is
Nodepod is a browser-native Node.js runtime. In plain English: it makes your browser act like a computer that can run real code.
It's basically a miniature operating system inside a browser tab. An in-memory filesystem replaces the kernel's VFS. A JavaScript execution engine replaces the Node.js runtime. 40+ polyfill modules replace Node's standard library. A custom shell interpreter replaces Bash. A package manager replaces npm. Web Workers replace OS processes. And a Service Worker bridge handles the networking.
The result: you can npm install express, write a server, and hit it with HTTP requests - all running inside a single browser tab.
And it does all of this with:
- No server - everything runs in the browser tab, nothing leaves your machine
- No cost - free and open source, forever. No license fees, no usage limits, no commercial restrictions
- Instant startup - ready in about 100 milliseconds (that's 20-50x faster than WebContainers)
- Tiny footprint - ~600KB gzipped. Compare that to the multi-megabyte WebAssembly binaries other runtimes ship
The systems under the hood
We're not going to walk through every line of code, but here's a rundown of the major systems and how they fit together. The codebase is about 119 TypeScript files totaling around 33,000 lines of source code.
The virtual filesystem (MemoryVolume)
Every computer needs somewhere to store files. Nodepod has MemoryVolume - an in-memory tree structure that implements a POSIX-like filesystem API. It supports 30+ synchronous methods that mirror Node.js's fs module: reading, writing, directories, symlinks, permissions, the works.
Since it lives in memory, every operation takes less than a millisecond. It doesn't survive a page refresh on its own, but MemoryVolume can serialize its entire state into a snapshot - both a JSON format and a high-performance binary format using a flat ArrayBuffer. That binary snapshot is also how new Web Worker processes get their initial copy of the filesystem.
File watchers are built in too. When a file changes, watchers fire up the directory tree. This is what makes Vite's Hot Module Replacement work inside Nodepod - it watches files through this mechanism.
The execution engine (ScriptEngine)
This is the heart of the whole thing - about 3,720 lines of TypeScript. When you run a JavaScript or TypeScript file, the ScriptEngine reads it from the virtual filesystem, strips TypeScript annotations with regex-based transforms, converts ESM import/export syntax to CommonJS require()/module.exports via an acorn-based AST parser, wraps it in a sandboxed function, and runs it with eval().
Every module gets wrapped in a function that injects sandboxed globals - require, module, exports, process, console, __filename, __dirname. Browser globals like window and document are explicitly set to undefined so Node.js code doesn't accidentally try to use them. One key detail: the native Promise is replaced with a custom SyncPromise - more on that in a moment.
The module resolution follows Node's algorithm closely. It handles node: prefixes, checks 100+ entries in a built-in core modules registry, walks node_modules directories, probes file extensions (.js, .ts, .tsx, .mjs, .cjs, .json), reads package.json exports fields with conditional exports, and handles circular dependencies correctly.
40+ Node.js polyfills
Node.js ships with dozens of built-in modules. Browsers have none of them. Nodepod reimplements over 40 of these so that code written for Node.js works without changes.
File system (fs) - The most critical one. Built per-engine as a bridge to the MemoryVolume instance, with 30+ sync methods, async callback wrappers, streams, file watchers, and a full fs.promises namespace.
HTTP (http) - About 1,280 lines. Full IncomingMessage, ServerResponse, and Server implementations. When code starts a server on port 3000, it registers in an in-process server registry. Incoming requests (routed through a Service Worker or dispatched directly) hit that virtual server and flow through the real framework handler. Localhost requests from http.request() are intercepted and dispatched directly - no actual network involved.
Child process (child_process) - The most complex polyfill at about 2,460 lines. exec() runs commands through the shell, spawn() can delegate to child Web Workers, execSync() uses SharedArrayBuffer with Atomics.wait() for true blocking. It includes fast paths for common trivial commands like version checks and echo.
Streams, Buffer, EventEmitter, crypto, path, net, zlib, readline - all reimplemented. The EventEmitter has a lazy initialization pattern because Express uses Object.create(EventEmitter.prototype) which bypasses the constructor. The crypto polyfill delegates to the browser's Web Crypto API for the security-critical parts. Zlib uses pako for gzip/deflate and a CDN-loaded WASM module for brotli.
A handful of third-party packages that frameworks depend on - like chokidar, esbuild, rollup, and ws - can't natively work inside a browser, so when code imports them, Nodepod returns its own implementations instead. We keep this kind of module shimming to a minimum to stay as close to a pure Node.js experience as possible, but in some places there's just no way around it.
Modules that are genuinely impossible in the browser - like tls, dgram, cluster, http2 - throw on use rather than silently failing.
The sync/async bridge
This is probably the cleverest part. Node.js's require() is synchronous. Thousands of npm packages assume readFileSync() returns instantly. But in the browser, almost everything is asynchronous.
Nodepod solves this with two custom primitives:
SyncThenable - a lightweight Promise-like that already holds its resolved value. When you call .then() on it, it fires the callback immediately and returns another SyncThenable. No microtask queue, no event loop tick.
SyncPromise - a real Promise subclass that gets injected as the global Promise inside every module. It tracks synchronous resolution and has fast paths in .then(), .all(), .race(). When code does await readFileSync(...), and the data is already in memory, the await resolves on the spot.
For cases where async/await would force things through the native Promise pipeline (which can't be synchronously unwrapped), Nodepod has a stripTopLevelAwait transform that replaces await with a custom syncAwait() function and strips async from function declarations. This propagates through the entire require chain when needed.
For genuinely blocking operations like execSync(), Nodepod uses SharedArrayBuffer and Atomics.wait(). The calling Web Worker thread is truly suspended - no polling, no busy-waiting - until the child process writes its result to shared memory and wakes it up with Atomics.notify().
The process model
Each spawned process runs in its own Web Worker with its own MemoryVolume, ScriptEngine, and shell instance. A ProcessManager on the main thread orchestrates everything - spawning (up to 50 processes, 10 levels deep), signal propagation through process trees, VFS synchronization across workers, and HTTP request routing to the worker that owns a given port.
The filesystem stays in sync through a three-tier system: a binary snapshot transferred at spawn time, async messages for ongoing changes broadcast to all workers, and an optional SharedArrayBuffer-backed shared VFS for zero-copy synchronous reads.
A SyncChannel with 64 slots (16KB each) handles execSync() across workers. The parent worker posts a spawn-sync message and blocks on Atomics.wait(). The main thread spawns the child, collects its output, writes the result to the shared slot, and wakes the parent. True blocking, 120 second timeout.
The shell
We built a custom shell from scratch - about 3,500 lines of TypeScript. It starts instantly, no compilation step.
The pipeline is: expand command substitutions ($(...)) → tokenize (character-by-character scan with variable expansion, quoting, glob expansion) → parse (recursive descent into an AST of lists, pipelines, and commands) → interpret (dispatch to builtins, registered commands, or PATH search).
It supports 35+ built-in commands across five modules - file operations (cat, cp, mv, rm, mkdir, etc.), directory navigation (ls with full flag support, cd, pwd), text processing (grep, sed, sort, uniq, tr, cut, diff), search (find with -name/-type/-exec, xargs), and shell environment (export, env, which, test).
Plus pipes, redirections, conditional execution (&&, ||), variable expansion with defaults (${VAR:-default}), glob patterns, and command substitution.
The package manager
When you run npm install express, Nodepod fetches the package metadata from the real npm registry, resolves the full dependency tree (with full semver support - caret, tilde, ranges, the lot), downloads the tarballs, extracts them into the virtual filesystem, converts ESM packages to CommonJS via esbuild-wasm in a worker pool, and creates binary stubs in node_modules/.bin/.
Downloads and extraction happen in parallel - up to 12 packages at once, with dependency tree resolution running 8 concurrent branches. Installing Express with all its dependencies takes about 3-5 seconds. A cached install takes about 50 milliseconds.
The networking bridge
When a virtual server starts listening on a port, a Service Worker intercepts HTTP requests to special /__virtual__/{port}/... URLs and routes them back to the owning Web Worker via the ProcessManager. The response flows back through the same chain. This is what lets you see a live preview of your app in an iframe.
It also handles WebSocket connections via a BroadcastChannel relay, streaming responses, and falls back to real network fetches for anything that doesn't match a virtual server.
How it compares
| Nodepod | WebContainers | |
|---|---|---|
| Price | Free, forever | Undisclosed (potentially $27k+/year) |
| License | MIT | Partially proprietary |
| Startup time | ~100ms | ~2-5 seconds |
| Core size | ~600KB gzipped | Multi-MB WASM binary |
| Node.js polyfills | 40+ modules | ~95% compatibility |
| Server required | No | No |
| Vendor lock-in | None | Yes |
| Shell | Custom TypeScript (~3,500 lines) | WASM bash |
| Framework support | Express, Vite, React, Vue, Svelte, SolidJS, Lit, more | Express, Vite, Next.js, Nuxt |
Is WebContainers more compatible? Yes - it covers more edge cases. But for the vast majority of use cases - running frameworks, installing packages, previewing apps - Nodepod handles it. And the price difference is hard to ignore: free vs. thousands of dollars per year.
What works today
Nodepod already runs real frameworks and tools:
- Express, Hono, Elysia - full web server support with the virtual server registry and Service Worker bridge
- Vite - works with our esbuild, rollup, and chokidar polyfills for HMR
- React, Vue, Svelte, SolidJS, Lit - install from npm, build with Vite, preview the result
- Vinext - a drop-in Next.js replacement built on Vite. Works out of the box since it uses Vite under the hood instead of webpack (which is what blocks native Next.js for now)
- TypeScript - first-class support via regex-based type stripping, no tsconfig needed
- Full CLI experiences -
npx create-next-app,npm create vite, and other interactive scaffolding tools work with the full prompts and everything - Any pure-JavaScript npm package - if it doesn't need native C++ addons, it probably works
What it can't do (and why that's okay)
We believe in honesty. Here's what Nodepod can't do:
- No native addons - packages like
sharporbetter-sqlite3require compiled C++ code. That can't run in a browser. This affects a small percentage of npm packages - No persistence by default - the virtual filesystem lives in memory. Refresh the page and it's gone. But you can save and restore snapshots - Scelar does this automatically
- Not full bash - our shell handles the commands developers use day-to-day, but it doesn't support advanced bash scripting like arrays or brace expansion
- No real symmetric encryption -
createCipheriv/createDecipherivthrow. Hashing, HMAC, and random generation all work fine - No true sandboxing - code runs in the browser's JavaScript context. Fine for trusted code (like code you generate yourself), not suitable for running untrusted user code
For Scelar's use case - instantly previewing and testing the platforms our AI Workers generate - these trade-offs don't matter. We need fast, reliable code execution, and that's exactly what Nodepod delivers.
See it in action: wZed
To show what you can build with Nodepod, we made wZed - a full browser-native code editor inspired by Zed. No backend, no server, everything runs in your browser tab.
It has Monaco-powered editing with 20+ language grammars, n-way split panes you can drag to resize, an integrated xterm.js terminal that runs real Node.js commands through Nodepod, a file explorer that syncs with the virtual filesystem, and a live preview panel that auto-navigates when a server starts up.
You can open a terminal, run npm install express, write a server, and see it running in the preview pane - all without leaving the browser. The full interactive CLI experience works too, so npm create vite gives you the actual prompts and scaffolds a real project.
It also has a command palette (Ctrl+Shift+P), file finder (Ctrl+P), global text search, customizable keybindings with VSCode/JetBrains/Vim presets, and multiple color themes. It's basically a real IDE that happens to need zero installation.
wZed is the best way to see what Nodepod can do. Check it out on GitHub.
Why we made it open source
We could have kept Nodepod proprietary. It's a competitive advantage for Scelar. But we know what it feels like to be priced out of a tool you need - or worse, to not even be able to find out what it costs.
Nodepod is MIT licensed. No license fees. No commercial restrictions. No usage limits. No "contact sales for pricing."
If you're building anything that needs to run code in the browser - a playground, a tutorial platform, a documentation site with live examples, an AI coding tool - Nodepod is free to use without paying us a cent.
Check it out on GitHub, and take a look at wZed - our browser-native IDE built on top of it - to see what's possible.
