Runtime
WebJs runs on Node 24+ or Bun. The same app source runs on either; the framework picks a runtime-neutral path internally and only the listener shell, the type stripper, and a few built-ins differ. Deno is a planned target (the listener seam is already runtime-neutral), not yet supported. This page is the single reference for the per-runtime commands and differences; other pages link here rather than repeat them.
Related reading:
- Getting Started for scaffolding an app.
- No-Build Model for how source is served.
- Deployment for shipping a container.
Node vs Bun at a glance
| Area | Node 24+ | Bun | Deno |
|---|---|---|---|
| Install | npm install | bun install (required, like Node) | planned |
| Run | npm run dev / npm run start | bun run dev / bun run start | planned |
| Listener | node:http shell | native Bun.serve (about 1.9x req/s on the listening path) | planned |
| TypeScript stripping | built-in module.stripTypeScriptTypes | amaro | planned |
| SQLite driver | built-in node:sqlite | built-in bun:sqlite | planned |
| Hot reload | restart on change (the webjs dev supervisor) | in place, in one long-lived process | planned |
| WebSocket | the ws library | native Bun.serve (bridged to the same API) | planned |
| 103 Early Hints | yes | no (Bun.serve has no informational-response API) | planned |
| Dev edit to a page / layout | full reload (the dev restart replaces the process) | refreshes in place, no reload | planned |
The in-place dev refresh needs the server process to survive the edit, which is the whole of that last row. A page or layout never hydrates, so a freshly rendered page is the complete truth for it and the client router can swap it in without a reload, keeping your scroll position and the hydrated state of components outside the changed region. The server classifies the changed file and puts the verdict on the live-reload event, so it needs a process that is still alive to do the classifying.
On Bun the dev server stays ONE process for the whole session, so it gets the refresh. bun --hot re-runs the CLI on a change, and the first server keeps running and only re-reads the app's modules, so nothing starts twice and memory stays flat however many edits you make. App modules are loaded fresh from disk on every reload (an editor's atomic save included), and an edit to a 'use server' module no longer restarts the process. Only an edit to instrumentation.* or env.*, which run once per process, restarts the server. On Node, the webjs dev supervisor restarts the server process on a change under app, components, modules, lib, or actions, or to a root middleware file, and a fresh process holds no record of what changed, so those edits are a full reload. Two Node cases still refresh in place: an edit outside that watched set (db/schema.server.ts, a webjs.dev.watch content directory), and npm run dev -- --no-hot, which keeps the server in one process on either runtime. A component edit is a full reload everywhere by design, because customElements.define is once-per-tag and swapping fresh markup onto the old class would be worse than the reload.
The supervisor is part of webjs dev itself (it replaced node --watch), and it is built so the preview does not stay down. A file in a watched directory that the dev server cannot read or watch (for example the temporary file sed -i creates when another user runs it, or a file deleted while it was being watched) logs one file watcher skipped warning, and the server keeps serving and reloading. If the server process crashes, the supervisor starts it again on the next file change, or by itself after a short wait (0.5s, then up to 10s for repeated crashes). On Bun the same supervisor brings a crashed server back and restarts it for an instrumentation.* or env.* edit; every other edit reloads in place.
Either way the .ts stripping is position-preserving with no sourcemap, and the bytes the browser fetches are identical. The 103 Early Hints gap only costs a small first-load latency edge where your edge forwards 103, never correctness (the modulepreload hints still ship in the document head).
Node (the default)
Scaffold with webjs create my-app (Node is the default runtime). Then:
npm install
npm run dev # or: npm run start Node 24+ is required: the built-in TypeScript stripper (module.stripTypeScriptTypes, stable from Node 24) and recursive fs.watch need it. The CLI's assertNodeVersion() preflight enforces the floor.
Bun
Scaffold with webjs create my-app --runtime bun, or bun create webjs my-app (the runtime is auto-detected from the invoking package manager). Then:
bun install
bun run dev # or: bun run start A Bun app installs with bun install (like Node), then runs on Bun: its dev / start / db scripts force bun --bun, which overrides the webjs bin's Node shebang so the server runs on Bun, where it selects the native Bun.serve listener and strips types via amaro. The dependencies resolve from node_modules, the same as Node.
The install also gives editor type intelligence (the editor reads the .d.ts files in node_modules).
Install model and reproducibility
A Bun app commits a bun.lock (the Bun analog of package-lock.json) for reproducible, offline installs. The scaffold's Bun Dockerfile runs bun install and serves on Bun via CMD ["bun", "--bun", "run", "start"].
Future runtimes
The server's listener selection is a runtime-neutral seam: startServer chooses the Bun.serve shell on Bun and the node:http shell on Node through the same seam, which is designed to also host a Deno.serve or an embedded adapter later. When Deno support lands it will appear here. Edge runtimes with no filesystem are a separate, later target.