ADR-0035 Runtime & toolchain
Company standard runtime and toolchain — Bun for the runtime, package manager, workspaces and test runner, with Bun's built-in APIs preferred over packages; Oxlint, Oxfmt and Vite stay for what Bun does not do.
- Decision
- Bun, built-ins firstShip & run
- Status
- Accepted
- Date
- Scope
- Company-wide default for all apps and tooling
Connections
Builds on
Context
Section titled “Context”ADR-0006 chose Node LTS, pnpm and Vite+, and rejected Bun as “not the reference runtime”. Since then Bun 1.4 has shipped, and it now covers several things we install packages for:
- One binary runs the app, installs dependencies, runs workspace scripts and runs tests
- Built-in clients: a PostgreSQL client (
Bun.SQL), an S3 client (Bun.S3Client), a Redis client, password hashing (Bun.password, argon2id),.envloading, a shell (Bun.$), globbing and hashing - Workspaces and catalogs in the root
package.json, isolated installs by default for new workspaces, and lifecycle scripts blocked unless a package is listed intrustedDependencies - The ecosystem we use runs on it: Bun documents TanStack Start (Nitro’s
bunpreset) and Drizzle, Drizzle v1 has abun-sqldriver, Better Auth runs on Bun, BullMQ lists Bun, and Testcontainers runs a Bun smoke test in CI - An official distroless image,
oven/bun:<version>-distroless, with no shell and no package manager
Each package Bun makes unnecessary is one less dependency to update and audit. Bun’s gaps are real too, and they decide where the built-ins stop.
Options considered
Section titled “Options considered”1. Keep Node LTS + pnpm + Vite+ (ADR-0006)
Section titled “1. Keep Node LTS + pnpm + Vite+ (ADR-0006)”Pros: the reference runtime; OpenTelemetry auto-instrumentation works; Vitest’s real-browser mode; an LTS line. Cons: every driver and client is a package; three tools to install and pin (Node, pnpm, Vite+); Vite+ is young and its CLI is one more thing to learn.
2. Bun as the package manager only, on Node
Section titled “2. Bun as the package manager only, on Node”Pros: fast installs, keeps Node’s compatibility. Cons: two tools again, and none of the runtime built-ins — most of the gain is lost.
3. Bun for runtime, packages, workspaces and tests
Section titled “3. Bun for runtime, packages, workspaces and tests”Pros
- One tool from laptop to production image
- Built-in Postgres, S3, Redis and password APIs replace driver and SDK packages
bun.lockis text and diffable; lifecycle scripts are opt-in per package- An official distroless runtime image
Cons
- Little OpenTelemetry auto-instrumentation. Since 1.4, OTel’s
httpandfsinstrumentations work on Bun fornode:httpandnode:fs. But Start’s server runs onBun.serve, notnode:http;Bun.SQLand Bun’sfetchare not libraries any instrumentation patches; and Bun treatsmodule.register, the ESM loader hook, as a no-op. Native tracing (Bun.otel) is an open pull request (oven-sh/bun#39965). Request, query andfetchspans come from three small wrappers of ours until it lands (ADR-0015) - No real-browser test mode.
bun testruns components in happy-dom; focus, keyboard and pointer behavior moves to Playwright (ADR-0014) - No linter, formatter or type checker: Oxlint, Oxfmt and
tscstay, as plain dev dependencies - Not a replacement for Vite: TanStack Start is built on Vite, so
vite buildstays, run by Bun - No LTS line: one release train, so the version is pinned exactly and upgraded deliberately
- Node-compatibility gaps:
node:modulehooks, parts ofnode:async_hooks, somenode:worker_threadsoptions. Playwright runs on Bun since 1.4, but Playwright’s own project lists Bun support as not planned
4. Deno
Section titled “4. Deno”Pros: secure-by-default permissions, built-in tooling. Cons: no documented TanStack Start setup, and npm compatibility is a mode rather than the default.
Decision
Section titled “Decision”Bun is the company runtime, package manager, workspace tool and test runner, in local development, in CI and in the production image (ADR-0003). When Bun has a built-in for something, we use it instead of a package — except where the built-in cannot meet another ADR’s requirement; those exceptions are listed under “Where a package stays”.
Concretely:
-
Version: Bun 1.4.x (1.4.4 at time of writing), pinned exactly in the root
package.jsonas"packageManager": "bun@1.4.4". CI installs that version, and the Docker images use the same version, by digest — the runtime image isoven/bun:<version>-distroless, run as a non-root user (ADR-0016). Renovate upgrades the field and the image tags together -
Packages:
bun install;bun.lockis the only lockfile and is committed; CI and Docker runbun ci, which fails when the lockfile is out of date. Never npm, pnpm or yarn -
Workspaces:
workspaces.packagesandworkspaces.catalogin the rootpackage.json(ADR-0026), with the isolated linker.bun run --filtertargets one package -
Lifecycle scripts stay blocked.
trustedDependenciesis empty by default; adding a package there is reviewed like a new dependency. Pre-1.0 packages are added withbun add --exact -
Tasks are
package.jsonscripts, run withbun run:Script Runs devbun --bun vite devbuildbun --bun vite buildcheckoxfmt --check . && oxlint && tsc --noEmitfixoxfmt . && oxlint --fixtestbun test(ADR-0014) -
Built-ins we use instead of packages:
Need Built-in Replaces Postgres client Bun.SQL, throughdrizzle-orm/bun-sql(ADR-0008)pgS3-compatible storage Bun.S3Client(ADR-0030)@aws-sdk/client-s3, presignerPassword hashing Bun.password, argon2id (ADR-0009)Better Auth’s default scrypt Tests, mocks, coverage bun test(ADR-0014)Vitest, @vitest/coverage-v8.envfiles in developmentautomatic loading dotenvScripts, files, globs, hashes Bun.$,Bun.file,Bun.Glob,Bun.hashexeca,fs-extra,glob -
Where a package stays, and why:
ioredis—rate-limiter-flexibleand@better-auth/redis-storageaccept only the clients they list, and Bun’s Redis client is not one of them; one client for Valkey beats two (ADR-0010)- Vite — TanStack Start is a Vite plugin (ADR-0002)
- Oxlint, Oxfmt, TypeScript — Bun has no linter, formatter or type checker
- Nodemailer and pino — Bun has no SMTP client and no logger (ADR-0029, ADR-0015)
- The Markdown pipeline —
Bun.markdownis marked unstable (ADR-0021) - Start’s
createCsrfMiddleware()— it checksSec-Fetch-SiteandOriginwithout tokens;Bun.CSRFissues tokens, a different design (ADR-0013) - Playwright —
Bun.WebViewdrives a headless browser, but has no test runner, traces, cross-browser runs or axe integration; Playwright runs on Bun since 1.4 (ADR-0014)
-
Server code imports from
bunonly insidesrc/server/(ADR-0026), so the browser build never sees it
The deciding factors: one tool instead of three, fewer packages to keep current, built-in Postgres and S3 clients, lifecycle scripts blocked by default, and an official distroless image. We accept maintaining our own request, query and fetch spans until Bun’s native tracing lands, component tests in a simulated DOM, and a runtime without an LTS line.
Consequences
Section titled “Consequences”Easier:
- A laptop, a CI runner and the image need one binary, at one pinned version
- Fewer dependencies: no Postgres driver, no AWS SDK, no test runner, no
dotenv - Installs are fast, and no package runs install scripts unless we listed it
Harder:
- Request, query and
fetchspans are our own wrappers untilBun.otelships (ADR-0015); Valkey,node:http, jobs, AI calls and logs use maintained instrumentation. Revisit when it ships - A Node-compatibility gap in a dependency is ours to work around or report
- Bun moves fast: read the release notes on every upgrade, and run the full test suite before merging it
- Examples online for our libraries mostly assume Node and pnpm
Given up: Node’s reference-runtime status and maintained OpenTelemetry instrumentation for the HTTP server, the Postgres client and fetch, Vitest’s real-browser mode, and Vite+’s single vp command. Accepted — one runtime with built-in clients is simpler to run and to secure than a runtime plus the packages it lacks.

