ADR-004: Monorepo with Bun Workspaces

Why Owlat uses a monorepo with Bun workspaces and Turborepo for fast installs, atomic cross-package changes, and cached CI.

  • Status: Accepted
  • Date: 2024-06-15

Context

Owlat consists of multiple interconnected pieces: a Nuxt frontend, a Convex backend, a documentation site, an email builder component, an email renderer library, shared types, and publishable SDKs. These packages share types and utilities extensively — for example, both the email builder and the email renderer depend on @owlat/shared for block type definitions.

The main options:

  1. Separate repositories — each package in its own repo with published npm packages for shared code. Clean boundaries but slow iteration: changing a shared type requires publishing, bumping versions, and updating consumers.
  2. Monorepo with npm/pnpm workspaces — colocated code with symlinked dependencies. Well-established but slower installs and more complex tooling.
  3. Monorepo with Bun workspaces — same workspace model but with Bun's fast install, native TypeScript execution, and Turbo for task orchestration.

Decision

Use a monorepo with Bun workspaces and Turborepo for task orchestration. All apps and packages live in a single repository under apps/ and packages/.

Structure:

apps/web        — @owlat/web (Nuxt frontend)
apps/api        — @owlat/api (Convex backend)
apps/docs       — @owlat/docs (VitePress)
packages/shared — @owlat/shared (types, utilities)
packages/email-builder   — @owlat/email-builder
packages/email-renderer  — @owlat/email-renderer
packages/sdk-js          — @owlat/sdk-js
packages/ui              — @owlat/ui

Consequences

Enables:

  • Atomic cross-package changes — a type change in shared and its consumers ship in one PR
  • Fast installs — Bun's linker is significantly faster than npm/pnpm for workspace resolution
  • Turbo caching — bun run ci:test only re-runs tests for packages affected by changes
  • Single CI pipeline — test.yml runs all workspace tests in one workflow
  • Shared tooling — one oxlintrc.json, one oxfmtrc.json, one Turbo config
  • Changesets for SDK publishing — @changesets/cli manages versioning for publishable packages

Trade-offs:

  • All contributors need Bun installed (not just Node)
  • Large bun.lock file that can cause merge conflicts on dependency changes
  • Turbo adds a build orchestration layer that new contributors need to understand
  • CI runs all workspace checks even for single-package changes (mitigated by Turbo caching)