ADR-0003 Deploy target
Company standard deploy target — Docker containers running Bun, self-hosted.
- Decision
- Docker, self-hosted, on BunShip & run
- Status
- Accepted
- Date
- Scope
- Company-wide default for all new apps
Connections
Context
Section titled “Context”Our apps are React + TanStack Start (ADR-0001, ADR-0002) running on Bun (ADR-0035) — full-stack: SSR, server functions, and the API routes (ADR-0036) ship from one codebase. The deploy target is decided before database, rate limiting, and auth because it constrains all of them:
- Database — edge isolates (Cloudflare Workers) can open TCP sockets to Postgres but can’t keep a long-lived connection pool, so they need a pooling proxy (Hyperdrive); our own containers pool directly.
- Rate limiting — Vercel/Cloudflare offer edge-native primitives; on our own containers we build it ourselves.
- Auth — session storage and middleware behavior differ per platform.
- Cost model — per-request pricing (serverless) vs flat monthly (containers) changes what “cheap” means at our scale.
TanStack Start deploys to all candidates (official adapters/presets exist for each), so the choice is about the platform, not framework compatibility.
Options considered
Section titled “Options considered”1. Vercel
Section titled “1. Vercel”Framework-agnostic serverless: Node and edge functions, per-app git previews, zero-ops.
Pros
- Zero infrastructure work; deploy on push, preview per PR
- Node runtime available → direct Postgres connections work fine
- Good DX: logs, analytics, rollback in the dashboard
- Scales to zero and up without thought
Cons
- Most expensive option at sustained traffic; per-request and bandwidth pricing
- Function execution limits (duration, memory) shape app design
- Vendor lock-in pressure around platform features
- Team-wide cost grows with seats
2. Cloudflare (Workers/Pages)
Section titled “2. Cloudflare (Workers/Pages)”TanStack Start deploys to Cloudflare via the Vite plugin; V8 isolate edge runtime.
Pros
- Cheapest at scale; generous free tier
- Globally low latency by default
- Strong platform primitives for later ADRs: KV, D1, R2, Durable Objects, Queues, native rate limiting
Cons
- No long-lived connection pool — Workers can open TCP sockets (
pgworks withnodejs_compat), but isolates are short-lived, so Postgres needs Hyperdrive in front for pooling. Biggest ripple effect on the DB ADR. - V8 isolate constraints: limited Node API surface, no long-running connections, CPU time limits
- Isolates are their own runtime with their own quirks — not the Bun runtime we standardized on (ADR-0035)
- Debugging/observability is weaker than a plain server process
3. Docker container (self-hosted), Bun runtime
Section titled “3. Docker container (self-hosted), Bun runtime”The TanStack Start server running on Bun, packaged as a Docker image, run on our own infra (VM, Fly.io/Railway/Render, or any container host). This is the decision below.
Pros
- A full server runtime: any database driver, any npm package Bun supports, websockets, long requests
- Same runtime in dev, CI, and prod — the Bun version pinned in
package.jsonon laptops and CI, the matching image tag in the Dockerfile (ADR-0035) - Flat, predictable cost; cheapest at small, steady load
- No platform constraints — every later decision (DB, rate limiting, queues) has maximum freedom
- Deployable anywhere; least lock-in; same image runs locally, in CI, and in prod
Cons
- We own ops: process management, TLS, scaling, deploys, health checks
- No per-PR previews or global edge without extra work
- Scaling beyond one box is a project, not a checkbox
Decision
Section titled “Decision”Docker container (self-hosted) running the Bun runtime is the company-standard deploy target for all new apps. (Amended 2026-11-13: this ADR first named Node LTS; ADR-0035 replaced it with Bun.)
The TanStack Start server runs on Bun inside a Docker image, deployed wherever we choose. The deciding factors: full runtime freedom for the decisions that follow (direct Postgres, custom rate limiting, any npm package), one pinned runtime across dev/CI/prod, flat predictable cost, no vendor lock-in, and one artifact that runs identically on a laptop, in CI, and in production. We accept owning ops in exchange for maximum freedom and control.
Consequences
Section titled “Consequences”Easier — what this unlocks for later ADRs:
- Database: direct TCP Postgres (or any DB) from the app container — no proxy, no edge constraint
- Rate limiting: we build it in-app as middleware — full control, no platform dependency
- Auth: any session strategy works (cookies, DB sessions, JWT) — no runtime limits
- Local dev = prod:
docker compose upmirrors production exactly, Bun version pinned inpackage.jsonand the Dockerfile image tag (ADR-0035)
Harder — what we now own:
- TLS termination, health checks, rolling deploys, and restart policy are our responsibility (compose / reverse proxy / orchestrator)
- Horizontal scaling requires an explicit plan when a product outgrows one box
- Per-PR preview environments need to be built if we want them
- We own the Bun image and its upgrade cadence (ADR-0035); its tag must match
packageManagerinpackage.json, and Renovate bumps both in one pull request (ADR-0016)
Given up: Vercel’s zero-ops DX and Cloudflare’s free global edge. Accepted — control, runtime consistency, and cost predictability matter more to us than managed convenience at this stage.

