ADR-0010: Monorepo with pnpm Workspaces
Status
Accepted - 2025-01-26
Context
TVL Platform consists of multiple packages (database, API services, frontend, workers, shared libraries) that need to work together. We need to decide how to organize code:
Business Requirements
- Enable code sharing (types, utilities, domain logic)
- Fast development iteration (change shared code, see effects immediately)
- Atomic commits (changes across multiple packages in single commit)
Technical Requirements
- Type safety across packages (TypeScript)
- Consistent dependency versions
- Efficient CI/CD (only build/test changed packages)
- Support 10-20 packages initially
Constraints
- pnpm already selected (ADR-0007)
- TypeScript 5.3+ (ADR-0005)
- Small team (2-5 engineers)
- Must work with Nx build system (ADR-0011)
Decision
Monorepo using pnpm workspaces to manage all packages in a single repository.
Structure
tvl-platform/ (root)
├── packages/           # Shared libraries
│   ├── database/       # Drizzle ORM, migrations
│   ├── shared/         # Shared types, utilities
│   ├── events/         # Domain events
│   └── validation/     # Zod schemas
│
├── services/           # Backend services
│   ├── api/            # Fastify API server
│   ├── workers/        # Background workers (BullMQ)
│   └── connectors/     # Channel integrations
│
├── apps/               # Frontend applications
│   └── web/            # Next.js web app
│
└── integrations/       # Third-party integrations
    ├── hostaway/       # Hostaway connector
    ├── stripe/         # Stripe payment integration
    └── sendgrid/       # Email service integration
Workspace Configuration
# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'services/*'
  - 'apps/*'
  - 'integrations/*'
Rationale
- Code Sharing: Shared packages (types, utils, events) used by all services
- Atomic Changes: Single commit updates API + frontend + database
- Type Safety: TypeScript enforces contracts across packages
- DRY Principle: Avoid duplicating code across repositories
- Simplified Dependencies: One lock file, consistent versions
Alternatives Considered
Alternative 1: Polyrepo (Multiple Repositories)
Rejected
Pros:
- Independent deployment (each repo deployed separately)
- Clear ownership (teams own repositories)
- No monorepo tooling needed
Cons:
- Code Duplication: Types, utilities copied across repos
- Breaking Changes: Hard to coordinate changes across repos
- Version Hell: Different repos use different versions of dependencies
- Slow Development: PRs across repos for single feature
- Complex CI/CD: Coordinate deployments across repos
Decision: Monorepo simplifies development for small team.
Alternative 2: Lerna Monorepo
Rejected
Pros:
- Popular monorepo tool
- Workspace support
- Versioning and publishing support
Cons:
- Slow: Lerna is slower than native pnpm workspaces
- Overhead: Adds complexity (unnecessary for our use case)
- Maintenance: Lerna maintenance has slowed (community concerns)
- Redundant: pnpm workspaces provide same functionality
Decision: pnpm workspaces simpler and faster.
Alternative 3: Nx Monorepo (without pnpm workspaces)
Rejected
Pros:
- Excellent build caching
- Dependency graph visualization
- Affected detection (only test changed packages)
Cons:
- Redundant: Nx works WITH pnpm workspaces (not instead of)
- We use both: pnpm for package management, Nx for build orchestration
Decision: Use pnpm workspaces + Nx together (ADR-0011).
Consequences
Positive
- 
Code Sharing - Shared types across frontend and backend
- Shared utilities (date formatting, validation)
- Domain events consumed by multiple services
 
- 
Atomic Changes - Single PR updates API, frontend, and database
- No coordination across repos
- Example: Add field to Bookingtype → Update API + frontend in one commit
 
- 
Type Safety - TypeScript enforces contracts
- Change shared type → Compiler errors everywhere (good!)
- Refactoring is safe (IDE renames across packages)
 
- 
Simplified CI/CD - One pipeline for all packages
- Nx caches builds (only rebuild changed packages)
- Consistent test/lint/build commands
 
- 
Developer Experience - Clone one repo, get entire platform
- Fast iteration (hot reload across packages)
- Autocomplete works across packages (TypeScript)
 
Negative
- 
Repository Size - Single large repository (vs. many small repos)
- Mitigation: Git handles large repos well, use sparse checkout if needed
 
- 
CI/CD Complexity - Must test all packages (or use affected detection)
- Mitigation: Nx provides affected detection (only test changed)
 
- 
Merge Conflicts - Multiple teams working in same repo
- Mitigation: Clear domain boundaries reduce conflicts
 
- 
Deployment Coupling - All packages versioned together
- Mitigation: Independent deployment possible later (Docker containers)
 
Package Dependencies
Internal Dependencies
// services/api/package.json
{
  "name": "@tvl/api",
  "dependencies": {
    "@tvl/database": "workspace:*",
    "@tvl/shared": "workspace:*",
    "@tvl/events": "workspace:*",
    "@tvl/validation": "workspace:*"
  }
}
workspace:* means "use local version" (pnpm resolves to local packages).
Dependency Graph
apps/web
  ↓
services/api
  ↓
packages/database
packages/shared
packages/events
packages/validation
Rule: Packages can depend on packages below them (acyclic).
Common Commands
Install Dependencies
# Install all packages
pnpm install
Add Dependency
# Add to specific package
pnpm --filter @tvl/api add fastify
# Add to workspace root
pnpm add -w typescript
Run Commands
# Run in all packages
pnpm -r build
pnpm -r test
pnpm -r lint
# Run in specific package
pnpm --filter @tvl/api dev
pnpm --filter @tvl/database migrate
Build Affected Packages (Nx)
# Only build packages affected by changes
nx affected:build
# Only test affected packages
nx affected:test
Package Naming Convention
Format: @tvl/{package-name}
{
  "name": "@tvl/database",
  "name": "@tvl/api",
  "name": "@tvl/web",
  "name": "@tvl/shared"
}
Scoped packages (@tvl/) prevent naming conflicts with npm packages.
Publishing Strategy
Internal Packages (Never Published)
- @tvl/database
- @tvl/api
- @tvl/web
- All services and apps
Reason: Internal use only, not npm packages.
Shared Libraries (Private npm - Future)
- @tvl/shared
- @tvl/events
- @tvl/validation
If needed: Publish to private npm registry (GitHub Packages or Verdaccio).
Migration Path
Phase 1: Monorepo (MVP.0 - Current)
- All packages in single repo
- pnpm workspaces
- Nx for build caching
- Deployed as monolith
Phase 2: Independent Deployment (V1.0 - Future)
- Packages still in monorepo
- Deploy as separate Docker containers
- API + Workers deployed independently
- Frontend deployed to Vercel
Phase 3: Microservices (V2.0+ - If Needed)
- Extract services to separate repos
- Shared packages published to private npm
- Independent teams own services
- API gateway for routing
Trigger: Team > 15 engineers, clear service boundaries.
Validation Checklist
-  pnpm-workspace.yamlconfigured
-  All packages use @tvl/*namespace
-  Internal dependencies use workspace:*
- Nx configured for build caching
- TypeScript path aliases configured
- CI/CD builds all packages
- Affected detection enabled (Nx)
- Dependency graph is acyclic