==> read the decisions
ADR-0011: Bun is the runtime and the toolchain, everywhere
Status: Accepted (2026-10-14). Implemented 2026-10-15.
Context
Section titled “Context”The platform runs on Node with npm: Express and csrf-csrf in the API, Next.js for the app, esbuild
in the shared build, node:sqlite for data, package-lock.json read by nixpkgs' importNpmLock.
That is five tools for what one runtime now does, and each has its own resolution rules -- the
shared/ tree resolves react only through a hand-made hoist of four symlinks.
Decision
Section titled “Decision”- Bun runs every process: the API, the Astro server (ADR-0012), every user's service host, and
the
cs-adminCLI. - Bun is the package manager:
bun.lockin every repo,package-lock.jsongone. - Bun is the bundler:
Bun.buildreplaces esbuild in the shared build. Every process ships as one bundle (bun build --target=bun): the store path holdsmain.jsand its source map, and there is nonode_modulesat runtime. Astro's server output is bundled the same way (verified: one file, about 1.1 MB, run by Bun with nonode_modules; the bundle must sit in aserver/directory besideclient/). - Bun's own APIs, not Node's:
Bun.servefor HTTP and WebSockets,Bun.sqlfor Postgres (ADR-0010; tagged templates are the default,unsafe(text, [$1])backsctx.db.query). - Hono is the API's router, on
Bun.serve. Express, cookie-parser and csrf-csrf are removed. CSRF stays the same design -- an HMAC double-submit token bound to the session hash, required on every state-changing cookie request -- as about forty lines of the platform's own. - Nix builds it with the platform's own
importBunLock, in pure Nix:bun.lockkeys are install paths and each entry carries its tarball's SRI hash, so a lockfile becomes onefetchurlper package and one derivation that unpacks them. No generated file in any repo, no import-from-derivation, no extra tool. Packages for another os/cpu are skipped; lifecycle scripts never run; git and file dependencies fail the build loudly. (bun2nix was the obvious choice and does not read Bun 1.4's lockfile v2; it is watched in the nixos2 backlog.) - Types are still checked by
tsc, because Bun strips types and does not check them.cs-check(tsc + eslint, shared lib/lint) stays a dependency of every release. TypeScript 7 (the Go compiler) is not adopted yet: checking takes about 0.6 s per project, and typed linting and Astro's checker depend on the TypeScript JS API. - The audit gate moves to
bun audit, andaudit-allow.jsonstays the record: each exception needs a GHSA id, a reason and a review date, and CI turns the live ones into--ignoreflags (Bun's only mechanism, CLI-only). - Bun comes from the fleet's nixpkgs pin (1.4.2 at the time of writing), never from a developer's machine, so the lockfile and the build agree on one version.
Consequences
Section titled “Consequences”- One resolution model for the whole tree; the
shared/react hoist disappears. - JavaScriptCore manages memory differently from V8, so every unit gets a
MemoryMaxand the dashboards show process memory (nixos2 ADR-0017). - systemd hardening keeps
MemoryDenyWriteExecuteoff for these units, as it is for Node: any JIT needs writable-executable pages. Whether Bun needs syscalls outside@system-serviceis checked when the units are written, not assumed. - A dependency that needs an install script (a native build) cannot be used until it is handled explicitly. Native code arriving as per-platform packages (rolldown, lightningcss, Tailwind's oxide) works unpatched (verified in the Nix sandbox).
Alternatives rejected
Section titled “Alternatives rejected”- Keep Express on Bun. It runs, through Bun's Node compatibility layer, but keeps the API on node:http semantics -- including a hand-written WebSocket upgrade proxy -- inside a runtime that has both natively.
bun build --compilesingle executables. Embeds a runtime per process; Nix already provides one.- A fixed-output fetch of the whole lockfile. A hand-kept hash per dependency change.