ADR-0007: pnpm as Package Manager for Monorepo
Status
Accepted - 2025-01-26
Context
TVL Platform uses a monorepo structure with multiple packages (database, API, workers, frontend, shared libraries). We need a package manager that:
Business Requirements
- Fast installation times (developer productivity)
- Reliable dependency resolution (no "works on my machine")
- Cost-effective disk usage (CI/CD minutes cost money)
Technical Requirements
- Workspace support for monorepo
- Strict dependency isolation (no phantom dependencies)
- Lock file for reproducible builds
- Compatible with Node.js 20 and TypeScript
- Fast CI/CD builds
Constraints
- Monorepo with 10-20 packages eventually
- Must work with Docker (Alpine Linux)
- Team familiar with npm/yarn
- GitHub Actions for CI/CD
Decision
pnpm 10.x as the package manager for the entire monorepo.
Configuration
# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'services/*'
  - 'apps/*'
  - 'integrations/*'
// package.json (root)
{
  "packageManager": "pnpm@10.19.0",
  "engines": {
    "node": ">=20.0.0",
    "pnpm": ">=8.0.0"
  }
}
Rationale
- Speed: 2-3x faster than npm/yarn (symlinks instead of copying)
- Disk Efficiency: Uses content-addressable storage (saves 50-70% disk space)
- Strict: Prevents phantom dependencies (packages must declare all dependencies)
- Workspace Support: First-class monorepo support
- Lock File: Deterministic, mergeable pnpm-lock.yaml
Alternatives Considered
Alternative 1: npm
Rejected
Pros:
- Built into Node.js (no extra install)
- Most familiar to developers
- Official package manager
Cons:
- Slower than pnpm (3x slower installs)
- Wastes disk space (duplicates packages)
- Allows phantom dependencies (not strict)
- Workspaces added late (npm 7, less mature)
Decision: Performance and strictness benefits outweigh familiarity.
Alternative 2: Yarn Classic (v1)
Rejected
Pros:
- Faster than npm
- Lock file (yarn.lock)
- Workspaces support
Cons:
- Deprecated (no longer maintained)
- Slower than pnpm
- Phantom dependencies allowed
- No longer recommended by Yarn team
Decision: Yarn team recommends Yarn Berry or pnpm instead.
Alternative 3: Yarn Berry (v3+)
Rejected
Pros:
- Plug'n'Play (PnP) mode very fast
- Modern architecture
- Good workspace support
Cons:
- PnP mode incompatible with many tools (TypeScript, ESLint had issues)
- Steep learning curve (PnP requires mental model shift)
- Smaller community than pnpm
- .pnp.cjsfile adds complexity
Decision: pnpm offers speed without PnP complexity.
Alternative 4: Turbo + npm
Rejected
Pros:
- Turbo provides caching (fast rebuilds)
- npm is familiar
Cons:
- Turbo doesn't replace package manager (still need npm/pnpm/yarn)
- npm still slow for installs
- We chose Nx for build orchestration (includes caching)
Decision: pnpm + Nx provides both fast installs and fast builds.
Consequences
Positive
- 
Performance - Install time: ~30 seconds (vs. 2-3 minutes with npm)
- CI/CD builds 2x faster
- Local development faster (quick installs)
 
- 
Disk Efficiency - Content-addressable store: Shared packages stored once
- Saves 50-70% disk space vs. npm
- Example: 10 projects with React → 1 copy instead of 10
 
- 
Correctness - Strict mode prevents phantom dependencies
- Forces packages to declare all dependencies
- Catches missing dependenciesinpackage.json
 
- 
Monorepo Support - First-class workspace support
- pnpm --filterto run commands in specific packages
- Recursive commands: pnpm -r test(run tests in all packages)
 
- 
Lock File - pnpm-lock.yamlis deterministic
- Merge-friendly (YAML format)
- Ensures same versions across all environments
 
Negative
- 
Learning Curve - Developers must learn pnpm CLI
- Different from npm (e.g., pnpm addvs.npm install)
- Mitigation: CLI is similar to npm, team adapts quickly
 
- 
Symlink Complexity - pnpm uses symlinks (can confuse debugging)
- Some tools don't follow symlinks correctly
- Mitigation: Modern tools handle symlinks well
 
- 
Ecosystem Support - Smaller community than npm
- Some tools assume npm/yarn
- Mitigation: pnpm compatible with npm ecosystem (uses node_modules)
 
- 
CI/CD Setup - Must install pnpm in CI
- Mitigation: Simple setup with GitHub Actions
 
pnpm Features Used
1. Workspaces
# Install dependencies for all packages
pnpm install
# Add dependency to specific package
pnpm --filter @tvl/database add drizzle-orm
# Run tests in all packages
pnpm -r test
# Run command in specific package
pnpm --filter @tvl/api dev
2. Strict Mode
// .npmrc
auto-install-peers=false
strict-peer-dependencies=true
Effect: Prevents phantom dependencies (packages must declare all dependencies).
3. Shared Scripts
// package.json (root)
{
  "scripts": {
    "test": "pnpm -r test",
    "lint": "pnpm -r lint",
    "build": "pnpm -r build",
    "db:migrate": "pnpm --filter @tvl/database migrate"
  }
}
4. Content-Addressable Store
~/.pnpm-store/
  v3/
    files/
      00/
        abc123def456... → react@18.2.0
Effect: All packages symlink to central store (saves disk space).
Implementation Guidelines
1. Installation
# Install pnpm globally (one-time)
npm install -g pnpm@10.19.0
# Or use Node.js Corepack (recommended)
corepack enable
corepack prepare pnpm@10.19.0 --activate
2. Dockerfile Setup
FROM node:20-alpine
# Enable pnpm via Corepack
RUN corepack enable pnpm
WORKDIR /app
# Copy dependency files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/*/package.json ./packages/
# Install dependencies
RUN pnpm install --frozen-lockfile --prod
# Copy source code
COPY . .
CMD ["node", "dist/server.js"]
3. GitHub Actions Setup
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
        with:
          version: 10
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm test
4. Common Commands
# Install all dependencies
pnpm install
# Add dependency to workspace root
pnpm add -w typescript
# Add dependency to specific package
pnpm --filter @tvl/api add fastify
# Remove dependency
pnpm remove axios
# Update dependencies
pnpm update
# Run script in all packages
pnpm -r build
# Run script in specific package
pnpm --filter @tvl/database test
Migration from npm
Step 1: Install pnpm
corepack enable
corepack prepare pnpm@10.19.0 --activate
Step 2: Import from package-lock.json
# Convert npm lockfile to pnpm
pnpm import
Step 3: Create Workspace File
# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'services/*'
  - 'apps/*'
Step 4: Update Scripts
{
  "scripts": {
    "test": "pnpm -r test"
  }
}
Step 5: Delete node_modules
rm -rf node_modules package-lock.json
pnpm install
Validation Checklist
-  pnpm-workspace.yamlconfigured
-  package.jsonhaspackageManagerfield
-  .npmrcconfigured for strict mode
-  pnpm-lock.yamlcommitted to git
- GitHub Actions uses pnpm
- Dockerfile uses pnpm
- All team members have pnpm installed
- No phantom dependencies (strict mode enforced)