Skip to content
sign in

==> read the decisions

ADR-0011: Bun is the runtime and the toolchain, everywhere

Status: Accepted (2026-10-14). Implemented 2026-10-15.

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.

  • Bun runs every process: the API, the Astro server (ADR-0012), every user's service host, and the cs-admin CLI.
  • Bun is the package manager: bun.lock in every repo, package-lock.json gone.
  • Bun is the bundler: Bun.build replaces esbuild in the shared build. Every process ships as one bundle (bun build --target=bun): the store path holds main.js and its source map, and there is no node_modules at runtime. Astro's server output is bundled the same way (verified: one file, about 1.1 MB, run by Bun with no node_modules; the bundle must sit in a server/ directory beside client/).
  • Bun's own APIs, not Node's: Bun.serve for HTTP and WebSockets, Bun.sql for Postgres (ADR-0010; tagged templates are the default, unsafe(text, [$1]) backs ctx.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.lock keys are install paths and each entry carries its tarball's SRI hash, so a lockfile becomes one fetchurl per 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, and audit-allow.json stays the record: each exception needs a GHSA id, a reason and a review date, and CI turns the live ones into --ignore flags (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.
  • One resolution model for the whole tree; the shared/ react hoist disappears.
  • JavaScriptCore manages memory differently from V8, so every unit gets a MemoryMax and the dashboards show process memory (nixos2 ADR-0017).
  • systemd hardening keeps MemoryDenyWriteExecute off for these units, as it is for Node: any JIT needs writable-executable pages. Whether Bun needs syscalls outside @system-service is 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).
  • 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 --compile single 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.