ADR-0002: Monorepo Structure with pnpm Workspaces and Turborepo
Last Updated: 2026-02-04 Status: Active Context: Decksmith
Context
Decksmith is designed as a modular system with multiple applications (web, API, worker, mobile) and shared business logic. We need a repository structure that supports:
- Shared code reuse between frontend and backend (contracts, domain logic)
- Independent deployment of applications
- Atomic refactoring across boundaries
- Type safety across the entire system
- Efficient build and test orchestration
The fundamental question is: monorepo or polyrepo, and if monorepo, which tooling?
Current Decision
We will use a monorepo structure with:
- pnpm workspaces for package management
- Turborepo for task orchestration and caching
- Structure:
apps/*for applications,packages/*for shared code - Workspace protocol (
workspace:*) to enforce dependency boundaries
Rationale
Why Monorepo Over Polyrepo
- Shared Contracts:
packages/schemadefines all DTOs. In a polyrepo, this would require publishing to npm or using git submodules, both adding friction. - Atomic Refactoring: Changes to
packages/domainthat affect multiple apps can be made and tested in a single commit. - Type Safety: TypeScript can resolve types across packages without publishing intermediate versions.
- Unified Tooling: Single CI/CD pipeline, single versioning strategy, shared configs.
- Developer Experience: Clone once, run once, test once.
This aligns with "Separation of concerns" (clear package boundaries) and "Explicit data contracts" (packages/schema is the single source of truth).
Why pnpm
- Efficient Disk Usage: Content-addressable storage, no duplicate dependencies.
- Strict by Default: No phantom dependencies. If a package uses a dependency, it must declare it.
- Fast Installation: Parallel dependency resolution, faster than npm/yarn.
- Workspace Protocol:
workspace:*ensures packages link to local versions, preventing accidental external dependencies. - Hoisting Control:
shamefully-hoist=falseprevents accidental access to undeclared dependencies.
pnpm's strictness aligns with "Clarity over cleverness" — explicit dependencies, no magic.
Why Turborepo
- Task Orchestration: Automatically parallelizes builds, tests, lints across packages.
- Intelligent Caching: Caches build outputs based on input hashes. Rebuilds only what changed.
- Dependency-Aware: Builds dependencies before dependents (
^buildsyntax). - Simple Configuration: Single
turbo.jsondefines all pipelines. - Incremental Adoption: Works with existing scripts, no rewrite required.
Turborepo aligns with "Deterministic behavior" — same inputs = same outputs, always.
Why Not Alternatives
Nx: More opinionated, heavier setup, designed for Angular-first (though supports others). Overkill for Decksmith's needs.
Lerna: Older, focused on versioning and publishing. No built-in caching or task orchestration. pnpm + Turbo is a more modern stack.
Rush: Microsoft's tool, very powerful but complex. Designed for massive monorepos (100+ packages). Decksmith has ~15 packages.
Polyrepo: Would require:
- Publishing
packages/schemato npm or private registry - Versioning every change to shared packages
- Coordinating updates across multiple repos
- Duplicated tooling configs
This would violate "Minimal coupling" by introducing npm as a dependency boundary.
Trade-offs
Benefits:
- Atomic changes: Refactor contracts and consumers in one PR
- Type safety: TypeScript resolves types across packages without intermediate builds
- Shared tooling: One prettier, one eslint, one tsconfig
- Fast CI: Turborepo caches unchanged packages
- Developer experience: Single
pnpm install, singlepnpm dev
Costs:
- Initial complexity: More setup than a single package.json
- Learning curve: Team must understand workspaces and dependency graphs
- CI time: Testing all packages on every push (mitigated by Turborepo caching)
- Discipline required: Must respect package boundaries to avoid coupling
Risks:
- Accidental coupling: Without discipline, packages may depend on implementation details, not interfaces
- Mitigation: ADR-0005 (Package Boundaries) defines strict dependency rules
- Slow builds: As the monorepo grows, builds could slow down
- Mitigation: Turborepo's caching and parallelization scale well
- Complex merges: Multiple developers changing shared packages
- Mitigation: Small, frequent PRs; clear ownership of packages
Evolution History
2026-02-04: Added pnpm Catalog for shared dependency versions
Decision: Use pnpm's catalog feature to centralize shared dependency versions.
What changed:
- Added
catalog:section topnpm-workspace.yamldefining shared versions - Packages reference shared dependencies with
"zod": "catalog:"instead of explicit versions - Affects:
zod,typescript,eslint,@types/node
Configuration (pnpm-workspace.yaml):
catalog:
zod: ^4.3.6
typescript: ^5.7.3
'@types/node': ^22.10.2
eslint: ^9.18.0Rationale:
- Single source of truth: All packages using the same dependency get the same version
- Easier upgrades: Update one line in catalog, all packages follow
- Explicit per-package dependencies: Each package still declares what it needs (better for bundlers)
- No accidental drift: Prevents different packages from accidentally using different versions
How it works:
- Catalog defines versions centrally in
pnpm-workspace.yaml - Each package declares dependencies with
"dep": "catalog:" - pnpm resolves
catalog:→ actual version at install time - Dependencies are still installed where declared (not hoisted to root)
Why not root package.json for all shared deps?
- Root is for workspace tooling (turbo, husky, prettier), not runtime deps
- Each package should declare its own runtime dependencies explicitly
- Better bundling: bundlers see exactly what each package needs
2026-01-08: Initial decision
- Chosen pnpm + Turborepo for monorepo infrastructure
- Defined apps/* and packages/* structure
- Enforced workspace protocol for local dependencies
References
- pnpm Workspaces Documentation
- pnpm Catalogs Documentation
- Turborepo Documentation
- Monorepo.tools - Comparison of monorepo tools
- ADR-0005: Package Boundaries and Dependency Graph (defines rules for using this structure)