Skip to content

Instantly share code, notes, and snippets.

@suntong
Created February 15, 2026 03:26
Show Gist options
  • Select an option

  • Save suntong/7481e3e703a51a6115556cf132e9b84c to your computer and use it in GitHub Desktop.

Select an option

Save suntong/7481e3e703a51a6115556cf132e9b84c to your computer and use it in GitHub Desktop.

Turborepo Monorepo — Comprehensive Guide

Section Outline (Table of Contents)

Below is the complete list of sections. Detailed expansion follows after.


Part I — Foundations

  1. What Is a Monorepo?
  2. What Is Turborepo?
  3. History & Evolution (Jared Palmer → Vercel Acquisition → Rust Rewrite)
  4. Problems Turborepo Solves
  5. Turborepo vs. Other Monorepo Tools (Nx, Lerna, Rush, Bazel, Moon)

Part II — Core Concepts

  1. Workspaces (The Building Block)
  2. The Dependency Graph (Topological Ordering)
  3. Tasks & the Task Pipeline (turbo.json)
  4. Hashing & Content-Aware Caching
  5. Local Caching (.turbo directory)
  6. Remote Caching (Vercel Remote Cache & Custom)
  7. Incremental Builds

Part III — Project Structure & Setup

  1. Recommended Directory Structure
  2. Package Manager Support (npm, yarn, pnpm, bun)
  3. Initializing a Turborepo (create-turbo & manual)
  4. Root package.json Configuration
  5. Root turbo.json Configuration
  6. Workspace package.json Conventions
  7. Workspace-Level turbo.json Overrides
  8. Internal Packages vs. Applications

Part IV — Configuration Deep Dive

  1. turbo.json Schema — Full Reference
  2. pipeline / tasks Configuration (v1 vs. v2 syntax)
  3. dependsOn — Task Dependencies & Topological Specifiers (^)
  4. outputs — Declaring Cacheable Artifacts
  5. inputs — Narrowing Hash Inputs
  6. cache — Enabling/Disabling Per Task
  7. persistent — Long-Running Tasks (Dev Servers)
  8. env & globalEnv — Environment Variable Handling
  9. passThroughEnv & globalPassThroughEnv
  10. outputLogs — Log Verbosity Control
  11. interactiveTask Configuration

Part V — CLI & Commands

  1. turbo run <task> — Running Tasks
  2. Filtering (--filter) — Scope, Directory, Git Diff
  3. --dry-run — Inspecting What Would Execute
  4. --graph — Visualizing the Task Graph
  5. --concurrency — Controlling Parallelism
  6. --continue — Running Past Failures
  7. --force — Bypassing Cache
  8. --output-logs — Controlling Output
  9. turbo prune — Sparse Monorepo Deploys
  10. turbo gen — Code Generation (Generators)
  11. turbo login / turbo link — Remote Cache Auth
  12. turbo ls & turbo inspect

Part VI — Caching System (In Depth)

  1. How Hashing Works (Files, Env Vars, Dependencies, Lockfile)
  2. Cache Hit vs. Cache Miss — Lifecycle
  3. Artifact Caching (File outputs, Logs)
  4. Local Cache Configuration & Garbage Collection
  5. Remote Caching Architecture
  6. Self-Hosted Remote Cache (Ducktail, turborepo-remote-cache, etc.)
  7. Cache Signing & Security
  8. Debugging Cache Misses (--verbosity, --dry-run=json)

Part VII — Dependency Management

  1. Internal Dependencies (workspace:* Protocol)
  2. External Dependency Hoisting & Isolation
  3. Shared Configuration Packages (ESLint, TypeScript, Prettier)
  4. Versioning Strategies Inside the Monorepo
  5. Managing Conflicting Dependency Versions

Part VIII — TypeScript in Turborepo

  1. Shared tsconfig Packages
  2. Project References vs. Workspace-Level Compilation
  3. Composite Builds & declaration Outputs
  4. Path Aliases & Module Resolution
  5. Type-Checking as a Turbo Task

Part IX — Building, Linting & Testing Pipelines

  1. Build Task Orchestration
  2. Linting with Shared ESLint Configs
  3. Unit Testing (Jest / Vitest) in a Monorepo
  4. End-to-End Testing Strategies
  5. Code Formatting (Prettier) as a Task

Part X — Development Workflow

  1. Running Dev Servers in Parallel (persistent tasks)
  2. Watch Mode & Rebuilds
  3. Hot Module Replacement Across Packages
  4. Using turbo with Docker
  5. turbo prune for Optimized Docker Layers
  6. CI/CD Integration (GitHub Actions, GitLab CI, CircleCI, etc.)
  7. Caching in CI — Remote Cache Best Practices
  8. Branch-Based & PR-Based Cache Scoping

Part XI — Advanced Patterns

  1. Polyglot Monorepos (Non-JS Packages)
  2. Publishing Packages from a Turborepo (Changesets)
  3. Feature Flags & Environment-Specific Builds
  4. Micro-Frontends with Turborepo
  5. Monorepo with Multiple Frameworks (Next.js, Remix, Vite, etc.)
  6. Dynamic Workspace Inclusion/Exclusion
  7. Custom Turbo Daemon & Background Processes
  8. Codemods & Migration Automation (@turbo/codemod)

Part XII — Performance & Scaling

  1. Benchmarks & Real-World Performance Gains
  2. Multitasking — How Turbo Parallelizes
  3. The Turbo Daemon (Persistent File-Watching Process)
  4. Scaling to Hundreds of Packages
  5. Profiling & Debugging Slow Pipelines

Part XIII — Ecosystem & Integrations

  1. Vercel Deployment Integration
  2. Next.js Monorepo Patterns
  3. Storybook in a Turborepo
  4. Prisma / Database Packages
  5. Shared UI Component Libraries (Design Systems)
  6. API & Full-Stack App Patterns

Part XIV — Troubleshooting & Common Pitfalls

  1. "Cache Miss" When You Expect a Hit
  2. Phantom Dependencies
  3. Circular Dependencies
  4. Platform/OS-Specific Cache Invalidation
  5. node_modules Resolution Issues
  6. Out-of-Memory & Large Monorepo Issues

Part XV — Migration & Adoption

  1. Migrating from Lerna
  2. Migrating from Nx
  3. Migrating from Polyrepo to Monorepo
  4. Incremental Adoption (Adding Turbo to Existing Monorepo)
  5. Turbo v1 → v2 Migration

Part XVI — Governance & Best Practices

  1. Naming Conventions
  2. Ownership & CODEOWNERS
  3. Monorepo Boundary Enforcement
  4. Documentation Strategies
  5. When NOT to Use a Monorepo / Turborepo


Full Detailed Expansion


Part I — Foundations

1. What Is a Monorepo?

A monorepo (monolithic repository) is a single version-controlled repository containing multiple distinct projects, applications, and/or libraries. Unlike a "monolith" (one tightly coupled application), a monorepo maintains separate, independently deployable/publishable units that happen to live together. Key properties include:

  • Unified version control — one repo, one commit history.
  • Shared tooling — one linting config, one CI pipeline, one set of standards.
  • Atomic commits — changes spanning multiple packages land in a single commit.
  • Code sharing — trivial to share utilities, types, and components.
  • Dependency deduplication — shared node_modules, reduced install size.

Notable companies using monorepos: Google (billions of lines), Meta, Microsoft, Vercel, Shopify.

2. What Is Turborepo?

Turborepo is a high-performance build system and task orchestrator for JavaScript/TypeScript monorepos. It is NOT a package manager, bundler, or framework — it is a task runner that sits on top of your existing toolchain and makes it dramatically faster.

Core value proposition:

  • Never do the same work twice — content-aware hashing + local/remote caching.
  • Maximum parallelism — understands the dependency graph and runs independent tasks concurrently.
  • Zero-config convention — works with your existing package.json scripts.
  • Incremental by design — only rebuilds what changed.

Turborepo is written in Rust (originally Go, rewritten starting in 2023) for maximum performance and ships as a single turbo binary.

3. History & Evolution

Year Event
2021 Jared Palmer creates Turborepo as an open-source project.
Dec 2021 Vercel acquires Turborepo. Jared joins Vercel.
2022 Rapid adoption. Remote caching integrated into Vercel platform.
2023 Rust rewrite begins (replacing Go internals). Turbo Daemon introduced.
2024 Turborepo v2 released. Pipeline config renamed to tasks. Major DX improvements.
2025 Continued Rust migration, performance improvements, enhanced filtering.

4. Problems Turborepo Solves

Problem How Turbo Solves It
Slow builds across many packages Caching + parallelism
Redundant CI work Remote cache shares artifacts across machines/branches
Complex task orchestration Declarative dependsOn with topological awareness
"Works on my machine" inconsistencies Deterministic hashing includes env vars, lockfile, etc.
Deploying only what changed --filter + turbo prune
Dev server coordination persistent tasks + parallel execution

5. Turborepo vs. Other Monorepo Tools

Feature Turborepo Nx Lerna (v6+) Rush Bazel
Language Rust TypeScript TypeScript TypeScript Java/Starlark
Learning curve Low Medium–High Low High Very High
Caching ✅ Local + Remote ✅ Local + Nx Cloud ✅ (via Nx)
Task orchestration
Code generation Basic (turbo gen) Extensive
Plugins ❌ Minimal ✅ Rich ecosystem
Framework-agnostic Mostly (Angular roots)
Config complexity Very Low (turbo.json) Medium (project.json) Low High Very High
Zero-config Partial
Polyglot Limited Limited ✅ Full
Computation caching
Distributed execution ✅ (Nx Agents)

Summary: Turborepo wins on simplicity and speed of adoption. Nx wins on feature richness and enterprise tooling. Bazel wins on polyglot and scale. Turborepo is the best choice when you want powerful caching/orchestration with minimal configuration overhead in a JS/TS ecosystem.


Part II — Core Concepts

6. Workspaces (The Building Block)

Workspaces are the atomic units of a monorepo. Each workspace is a directory with its own package.json. They can be:

  • Applications (apps/web, apps/api) — deployable artifacts.
  • Packages (packages/ui, packages/utils) — shared libraries consumed by apps or other packages.

Workspaces are declared via the package manager:

// Root package.json (npm/yarn)
{
  "workspaces": ["apps/*", "packages/*"]
}
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

Turborepo auto-discovers all workspaces. It does not maintain its own workspace registry.

7. The Dependency Graph (Topological Ordering)

Turborepo constructs a DAG (Directed Acyclic Graph) from workspace-level dependencies and devDependencies:

apps/web → packages/ui → packages/utils
apps/api → packages/utils

This graph determines:

  • Build orderpackages/utils must build before packages/ui, which must build before apps/web.
  • Parallelism opportunitiespackages/utils and unrelated packages can build simultaneously.
  • Cache invalidation scope — changes to packages/utils invalidate the cache for all dependents.

Turbo performs topological sorting to guarantee correct ordering.

8. Tasks & the Task Pipeline

A "task" in Turborepo is a script defined in a workspace's package.json:

{
  "scripts": {
    "build": "tsc",
    "test": "vitest",
    "lint": "eslint ."
  }
}

The task pipeline (defined in turbo.json) tells Turbo how tasks relate:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {},
    "dev": {
      "persistent": true,
      "cache": false
    }
  }
}

Key insight: Tasks are NOT workspaces. When you run turbo run build, Turbo runs the build script in every workspace that has one, respecting the dependency graph and pipeline configuration.

9. Hashing & Content-Aware Caching

For every task execution, Turbo computes a deterministic hash based on:

  1. Source files in the workspace (contents, not timestamps)
  2. Internal dependency hashes (transitive)
  3. Lockfile fragments (resolved external dependency versions)
  4. Environment variables declared in env / globalEnv
  5. Task configuration in turbo.json
  6. Arguments passed to the task
  7. Global files (if configured via globalDependencies)

If the hash matches a previous run → CACHE HIT → outputs are restored instantly. If not → CACHE MISS → task executes normally.

This is content-aware, not timestamp-based. Reverting a file to a previous state will produce a cache hit from that previous run.

10. Local Caching

By default, cached artifacts are stored in ./node_modules/.cache/turbo (or .turbo in newer versions). For every cached task, Turbo stores:

  • Output files (as declared in outputs)
  • Terminal logs (stdout/stderr)
  • Hash metadata

On a cache hit, outputs are replayed from the local cache and logs are replayed to the terminal (with a FULL TURBO indicator).

11. Remote Caching

Remote caching allows sharing the cache across machines: CI runners, teammates' laptops, and different branches.

Vercel Remote Cache (built-in):

turbo login      # Authenticate with Vercel
turbo link       # Link repo to a Vercel project
turbo run build  # Now caches are synced to the cloud

How it works:

  1. Before executing a task, Turbo checks the remote cache for the hash.
  2. If found → downloads artifacts → CACHE HIT (no execution).
  3. If not found → executes → uploads artifacts to remote cache.

This means a CI build on main can populate the cache, and a developer pulling the branch gets instant builds.

12. Incremental Builds

Turborepo provides incrementality at two levels:

  1. Task-level — only re-runs tasks whose inputs changed.
  2. Workspace-level — only affected workspaces are processed when using --filter.

Combined with caching, this means:

  • First run: all tasks execute (cold cache).
  • Subsequent runs: only changed tasks execute; everything else is restored from cache.
  • In CI: if remote cache is warm, even first runs on a new machine are instant.

Part III — Project Structure & Setup

13. Recommended Directory Structure

my-monorepo/
├── apps/
│   ├── web/                  # Next.js frontend
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── api/                  # Express/Fastify backend
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── mobile/               # React Native app
│       ├── src/
│       └── package.json
├── packages/
│   ├── ui/                   # Shared React component library
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── utils/                # Shared utility functions
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── database/             # Prisma schema + client
│   │   ├── prisma/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── eslint-config/        # Shared ESLint configuration
│   │   ├── index.js
│   │   └── package.json
│   └── typescript-config/    # Shared tsconfig files
│       ├── base.json
│       ├── nextjs.json
│       ├── react-library.json
│       └── package.json
├── turbo.json
├── package.json
├── pnpm-workspace.yaml       # (if using pnpm)
├── .gitignore
├── .npmrc
└── README.md

14. Package Manager Support

Turborepo supports all major Node package managers:

Manager Workspace Declaration Lockfile Notes
npm (v7+) package.jsonworkspaces package-lock.json Native workspaces
Yarn (v1 Classic) package.jsonworkspaces yarn.lock Widely used
Yarn (v2+ Berry) package.jsonworkspaces yarn.lock PnP support varies
pnpm pnpm-workspace.yaml pnpm-lock.yaml Recommended — strictest, fastest, most disk-efficient
Bun package.jsonworkspaces bun.lockb Emerging support

Turborepo auto-detects the package manager from the lockfile. You can also specify it via "packageManager" in root package.json:

{
  "packageManager": "pnpm@9.1.0"
}

15. Initializing a Turborepo

Option A: Scaffold with create-turbo

npx create-turbo@latest my-monorepo
# or
pnpm dlx create-turbo@latest my-monorepo

This scaffolds a fully working monorepo with example apps and packages.

Option B: Add to existing monorepo

# Install turbo
pnpm add -D turbo -w   # (-w = root workspace)

# Create turbo.json
cat > turbo.json << 'EOF'
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
EOF

Option C: Global install

npm install -g turbo
# Now `turbo` is available everywhere

16. Root package.json Configuration

{
  "name": "my-monorepo",
  "private": true,
  "packageManager": "pnpm@9.1.0",
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "format": "prettier --write \"**/*.{ts,tsx,md}\""
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "prettier": "^3.0.0"
  }
}

Key points:

  • Root should be "private": true (never published).
  • Root scripts delegate to turbo run.
  • Shared dev tools (prettier, turbo) live in root devDependencies.

17. Root turbo.json Configuration

This is the heart of Turborepo. Detailed breakdown in Part IV.

18. Workspace package.json Conventions

{
  "name": "@myorg/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
    "lint": "eslint src/",
    "test": "vitest run"
  },
  "dependencies": {
    "react": "^18.0.0"
  },
  "devDependencies": {
    "@myorg/eslint-config": "workspace:*",
    "@myorg/typescript-config": "workspace:*",
    "tsup": "^8.0.0",
    "typescript": "^5.0.0"
  }
}

The workspace:* protocol tells the package manager to resolve this dependency from the local monorepo, not npm.

19. Workspace-Level turbo.json Overrides

Individual workspaces can override the root turbo.json for their specific needs:

// apps/web/turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "extends": ["//"],
  "tasks": {
    "build": {
      "outputs": [".next/**", "!.next/cache/**"],
      "env": ["NEXT_PUBLIC_API_URL", "DATABASE_URL"]
    }
  }
}
  • "extends": ["//"] means "inherit from the root turbo.json".
  • Only override what's different for this workspace.

20. Internal Packages vs. Applications

Aspect Internal Package Application
Purpose Shared library Deployable artifact
Published to npm? Usually no ("private": true) No
Consumed by Other packages + apps End users
main/exports Yes (entry points) Usually no
Build output dist/ .next/, build/, etc.
Examples @myorg/ui, @myorg/utils apps/web, apps/api

"Just-in-time" Packages (an alternative pattern): Skip building internal packages entirely — let the consuming app's bundler transpile them:

// packages/ui/package.json
{
  "name": "@myorg/ui",
  "main": "./src/index.ts",  // Point directly to source!
  "types": "./src/index.ts"
}

Then configure Next.js (or your bundler) to transpile the package:

// apps/web/next.config.js
module.exports = {
  transpilePackages: ["@myorg/ui"]
};

This eliminates the need to build internal packages at all, dramatically simplifying the pipeline.


Part IV — Configuration Deep Dive

21. turbo.json Schema — Full Reference

{
  "$schema": "https://turbo.build/schema.json",
  
  // Global configuration
  "globalDependencies": ["tsconfig.json", ".env"],
  "globalEnv": ["CI", "NODE_ENV"],
  "globalPassThroughEnv": ["GITHUB_TOKEN"],
  
  // UI mode
  "ui": "tui",  // "tui" | "stream"
  
  // Daemon
  "daemon": true,
  
  // Remote cache configuration  
  "remoteCache": {
    "enabled": true,
    "signature": false,
    "preflight": false,
    "timeout": 60
  },
  
  // Task definitions
  "tasks": {
    "build": { /* ... */ },
    "test": { /* ... */ },
    "lint": { /* ... */ },
    "dev": { /* ... */ }
  }
}

22. pipeline / tasks Configuration

v1 syntax (deprecated):

{ "pipeline": { "build": { ... } } }

v2 syntax (current):

{ "tasks": { "build": { ... } } }

Migrate with: npx @turbo/codemod migrate

23. dependsOn — Task Dependencies

This is the most important configuration field. It defines what must complete before a task can start.

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "deploy": {
      "dependsOn": ["build", "test", "lint"]
    }
  }
}

Three types of dependencies:

Syntax Meaning Example
"^build" Topological — run build in all workspace dependencies first Package web depends on ui; ui#build runs before web#build
"build" Same-workspace — run build in the same workspace first test waits for build in the same package
"ui#build" Specific workspace — run build in workspace ui first Explicit cross-workspace dependency

Visual example of "dependsOn": ["^build"]:

packages/utils#build  ──→  packages/ui#build  ──→  apps/web#build
                       └──→  apps/api#build

24. outputs — Declaring Cacheable Artifacts

{
  "build": {
    "outputs": [
      "dist/**",
      ".next/**",
      "!.next/cache/**",
      "build/**",
      "coverage/**"
    ]
  }
}
  • Globs relative to the workspace root.
  • These files are saved to cache and restored on cache hit.
  • ! prefix for negation (exclude from caching).
  • If outputs is empty [], only logs are cached (useful for lint, test).

25. inputs — Narrowing Hash Inputs

By default, Turbo hashes all files in a workspace (excluding gitignored files). You can narrow this:

{
  "lint": {
    "inputs": [
      "src/**/*.ts",
      "src/**/*.tsx",
      ".eslintrc.*",
      "eslint.config.*"
    ]
  },
  "test": {
    "inputs": [
      "src/**",
      "test/**",
      "vitest.config.*"
    ]
  }
}

Benefit: Changing a README.md won't invalidate the lint cache if README.md isn't in inputs.

$TURBO_DEFAULT$ — a special token meaning "all default inputs":

{
  "test": {
    "inputs": ["$TURBO_DEFAULT$", "test-fixtures/**"]
  }
}

26. cache — Enabling/Disabling Per Task

{
  "dev": {
    "cache": false    // Never cache dev server runs
  },
  "deploy": {
    "cache": false    // Side-effect tasks shouldn't be cached
  },
  "build": {
    "cache": true     // Default — cache this task
  }
}

Disable caching for:

  • Dev servers
  • Deployment scripts
  • Database migrations
  • Any task with side effects

27. persistent — Long-Running Tasks

{
  "dev": {
    "persistent": true,
    "cache": false
  }
}

Persistent tasks:

  • Are expected to never exit (dev servers, watch modes).
  • Cannot be depended on by other tasks (since they never complete).
  • Keep the terminal alive.
  • Turbo runs them last and in parallel.

28. env & globalEnv

Turborepo is strict about environment variables to ensure cache correctness.

{
  "globalEnv": ["CI", "NODE_ENV"],
  "tasks": {
    "build": {
      "env": ["DATABASE_URL", "NEXT_PUBLIC_*"],
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}
  • env — workspace-task-level: included in the task's hash. Wildcards supported.
  • globalEnv — affects hash for ALL tasks across ALL workspaces.
  • If an env var is NOT listed → it is excluded from the hash and not available (in strict mode) to the task.

Framework auto-detection: Turbo automatically includes framework-prefixed env vars:

  • NEXT_PUBLIC_* for Next.js
  • VITE_* for Vite
  • REACT_APP_* for CRA
  • NUXT_* for Nuxt
  • PUBLIC_* for SvelteKit

29. passThroughEnv & globalPassThroughEnv

{
  "globalPassThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN"],
  "tasks": {
    "deploy": {
      "passThroughEnv": ["DEPLOY_TOKEN"],
      "cache": false
    }
  }
}

Pass-through env vars:

  • Are available to the task at runtime.
  • Are NOT included in the hash (won't affect caching).
  • Use for secrets that change per environment but shouldn't invalidate cache.

30. outputLogs — Log Verbosity Control

{
  "build": {
    "outputLogs": "new-only"
  }
}
Value Behavior
"full" Show all logs (default)
"hash-only" Only show hash
"new-only" Show logs only for cache misses; suppress replayed logs
"errors-only" Only show stderr/error output
"none" Suppress all output

31. interactiveTask Configuration

For tasks that need terminal interaction (prompts, stdin):

{
  "tasks": {
    "login": {
      "interactive": true,
      "cache": false,
      "persistent": true
    }
  }
}

Interactive tasks disable output buffering and connect stdin.


Part V — CLI & Commands

32. turbo run <task>

# Run build in all workspaces
turbo run build

# Run multiple tasks
turbo run build test lint

# Equivalent shorthand (turbo v2)
turbo build test lint

Turbo orchestrates tasks based on the dependency graph defined in turbo.json.

33. Filtering (--filter)

The --filter flag is one of Turborepo's most powerful features:

# By workspace name
turbo run build --filter=@myorg/web
turbo run build --filter=web

# By directory
turbo run build --filter=./apps/web

# By workspace + its dependencies
turbo run build --filter=web...

# By workspace + its dependents
turbo run build --filter=...ui

# By workspace + dependencies + dependents
turbo run build --filter=...ui...

# By git diff (changed since main)
turbo run build --filter=[main...HEAD]

# Combine: changed packages and their dependents
turbo run build --filter=...[main...HEAD]

# Exclude a workspace
turbo run build --filter=!@myorg/docs

# Multiple filters (union)
turbo run build --filter=web --filter=api

# By directory glob
turbo run build --filter="./packages/*"

Filter syntax cheat sheet:

Syntax Meaning
name Exact workspace match
name... Workspace + all its dependencies (downstream)
...name Workspace + all its dependents (upstream)
...name... Dependencies + workspace + dependents
./path By filesystem path
[git-range] Changed workspaces in git commit range
{dir} Directories within a workspace
!name Exclude

34. --dry-run

# Text output
turbo run build --dry-run

# JSON output (for scripting)
turbo run build --dry-run=json

Shows what tasks would run, their hashes, dependencies, and cache status — without executing anything. Invaluable for debugging.

35. --graph

# Open interactive graph in browser
turbo run build --graph

# Output to file
turbo run build --graph=graph.svg
turbo run build --graph=graph.png
turbo run build --graph=graph.pdf
turbo run build --graph=graph.json
turbo run build --graph=graph.dot

Generates a visual representation of the task dependency graph. Extremely useful for understanding execution order and parallelism.

36. --concurrency

# Number of concurrent tasks
turbo run build --concurrency=4

# Percentage of CPU cores
turbo run build --concurrency=50%

# Default: 10 (or number of CPUs, whichever is higher)

37. --continue

turbo run build test --continue

By default, Turbo stops on first failure. --continue runs as many tasks as possible, reporting all failures at the end. Useful in CI for getting complete error reports.

38. --force

turbo run build --force

Ignores existing cache and re-runs all tasks. Useful for debugging cache issues or ensuring a clean build.

39. --output-logs

turbo run build --output-logs=new-only
turbo run build --output-logs=errors-only

CLI override for the outputLogs config. Same options as Section 30.

40. turbo prune

turbo prune @myorg/web --out-dir=./out

Creates a sparse/pruned copy of the monorepo containing only:

  • The target workspace
  • All its internal dependencies (transitive)
  • A pruned lockfile
  • Relevant workspace configurations

Output structure:

out/
├── apps/
│   └── web/           # The target app
├── packages/
│   ├── ui/            # Direct dependency
│   └── utils/         # Transitive dependency
├── package.json       # Pruned root
├── pnpm-lock.yaml     # Pruned lockfile (only relevant deps)
└── pnpm-workspace.yaml

Primary use case: Docker builds — dramatically reduces Docker context size and layer invalidation:

FROM node:20-alpine AS builder
RUN npm install -g turbo pnpm

WORKDIR /app
COPY . .
RUN turbo prune @myorg/web --docker

# Thin install layer
FROM node:20-alpine AS installer
WORKDIR /app
COPY --from=builder /app/out/json/ .
RUN pnpm install --frozen-lockfile

# Build layer
COPY --from=builder /app/out/full/ .
RUN pnpm turbo run build --filter=@myorg/web

# Production
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=installer /app/apps/web/.next ./.next
COPY --from=installer /app/apps/web/package.json ./
CMD ["pnpm", "start"]

The --docker flag splits output into json/ (package.json files for install layer) and full/ (source code for build layer) to optimize Docker layer caching.

41. turbo gen — Code Generation

# Run a generator
turbo gen workspace    # Generate a new workspace from templates
turbo gen run          # Run custom generators

Define custom generators in turbo/generators/config.ts:

import type { PlopTypes } from "@turbo/gen";

export default function generator(plop: PlopTypes.NodePlopAPI): void {
  plop.setGenerator("react-component", {
    description: "Create a new React component",
    prompts: [
      {
        type: "input",
        name: "name",
        message: "Component name?",
      },
    ],
    actions: [
      {
        type: "add",
        path: "packages/ui/src/{{pascalCase name}}.tsx",
        templateFile: "templates/component.hbs",
      },
    ],
  });
}

42. turbo login / turbo link

# Authenticate with Vercel
turbo login

# Link to a Vercel project (for remote caching)
turbo link

# Unlink
turbo unlink

# Logout
turbo logout

43. turbo ls & turbo inspect

# List all workspaces
turbo ls

# Show detailed info about a workspace
turbo inspect @myorg/web

Part VI — Caching System (In Depth)

44. How Hashing Works

Turbo generates a hash for each workspace#task combination:

Hash = f(
  source files,              # Content of all files in the workspace
  internal dep hashes,       # Hashes of dependency workspaces (transitive)
  lockfile entries,          # Resolved versions of external dependencies
  task config,               # turbo.json config for this task
  env variables,             # Values of declared env vars
  global dependencies,       # Files listed in globalDependencies
  global env vars,           # Values of globalEnv vars
  turbo.json hash,           # The config file itself
  arguments                  # CLI args passed to the task
)

Important: Hashing is content-based, not timestamp-based. git checkout to a previous commit will produce cache hits from that commit's builds.

45. Cache Hit vs. Cache Miss — Lifecycle

Cache Miss:

1. Compute hash
2. Check local cache → not found
3. Check remote cache → not found
4. Execute task
5. Capture outputs + logs
6. Store in local cache (keyed by hash)
7. Upload to remote cache (if enabled)

Cache Hit (local):

1. Compute hash
2. Check local cache → FOUND
3. Restore outputs to workspace
4. Replay logs to terminal
5. Print "FULL TURBO" indicator

Cache Hit (remote):

1. Compute hash
2. Check local cache → not found
3. Check remote cache → FOUND
4. Download artifacts
5. Store in local cache
6. Restore outputs to workspace
7. Replay logs

46. Artifact Caching

Cached artifacts per task include:

  • File outputs — everything matching the outputs globs, stored as a compressed tarball.
  • Logs — stdout and stderr captured during execution.
  • Metadata — hash, timing, status information.

47. Local Cache Configuration & Garbage Collection

// turbo.json
{
  "cacheDir": ".turbo/cache",  // Custom cache directory
  "daemon": true               // File-watching daemon for faster hashing
}

Local cache can grow large. Clean it with:

# Remove cache
rm -rf node_modules/.cache/turbo
# or
rm -rf .turbo

48. Remote Caching Architecture

┌─────────────┐     ┌──────────────┐     ┌─────────────────┐
│ Developer A │────▶│  Remote Cache │◀────│   CI Runner     │
│  (laptop)   │     │   (Vercel)   │     │  (GitHub Action) │
└─────────────┘     └──────────────┘     └─────────────────┘
       ▲                    ▲                      │
       │                    │                      │
       │              ┌─────┴──────┐               │
       └──────────────│ Developer B│───────────────┘
                      │  (laptop)  │
                      └────────────┘

Artifacts are stored as content-addressable blobs keyed by task hash. The cache is team-scoped (via Vercel project/team).

49. Self-Hosted Remote Cache

Turbo's remote cache uses a simple HTTP API. Open-source servers:

Configuration:

# Set custom API endpoint
turbo run build --api="https://my-cache-server.com" --token="my-token" --team="my-team"

Or in .turbo/config.json:

{
  "apiurl": "https://my-cache-server.com",
  "teamid": "team_xxx",
  "token": "xxx"
}

50. Cache Signing & Security

// turbo.json
{
  "remoteCache": {
    "signature": true
  }
}

When enabled, Turbo signs cached artifacts with an HMAC key set via TURBO_REMOTE_CACHE_SIGNATURE_KEY env var. This prevents cache poisoning attacks.

51. Debugging Cache Misses

# Step 1: Dry run to see hashes
turbo run build --dry-run=json

# Step 2: Compare hashes between runs
# Look at the "hash" and "hashOfExternalDependencies" fields

# Step 3: Verbose mode
turbo run build --verbosity=2

# Step 4: Check what files are included
turbo run build --summarize
# Generates .turbo/runs/<run-id>.json with full details

Common causes:

  • Unlisted env var changing between runs
  • Timestamp in generated files
  • Non-deterministic build output
  • Different node_modules resolution
  • OS-specific lockfile differences

Part VII — Dependency Management

52. Internal Dependencies (workspace:*)

// apps/web/package.json
{
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:^1.0.0"
  }
}
Protocol Meaning Published form
workspace:* Any version from workspace Replaced with actual version on publish
workspace:^ Compatible version ^1.2.3
workspace:~ Patch-compatible ~1.2.3

These are resolved by the package manager at install time to symlinks/hard links to the local workspace directory.

53. External Dependency Hoisting & Isolation

pnpm (recommended):

  • Strict isolation by default — packages can only import what they declare.
  • Hoisting controlled via .npmrc:
    # .npmrc
    shamefully-hoist=false  # default, strict
    public-hoist-pattern[]=*eslint*
    public-hoist-pattern[]=*prettier*

npm/yarn:

  • Hoist by default — all deps accessible from any workspace (phantom dependency risk).
  • nohoist available in Yarn Classic.

54. Shared Configuration Packages

ESLint config package:

// packages/eslint-config/package.json
{
  "name": "@myorg/eslint-config",
  "version": "0.0.0",
  "private": true,
  "files": ["index.js", "next.js", "react.js"],
  "dependencies": {
    "@typescript-eslint/eslint-plugin": "^7.0.0",
    "@typescript-eslint/parser": "^7.0.0",
    "eslint-config-prettier": "^9.0.0"
  }
}
// packages/eslint-config/index.js
module.exports = {
  parser: "@typescript-eslint/parser",
  extends: ["eslint:recommended", "prettier"],
  // ...
};

Consumed by workspaces:

// apps/web/.eslintrc.json
{
  "extends": ["@myorg/eslint-config/next"]
}

TypeScript config package:

// packages/typescript-config/package.json
{
  "name": "@myorg/typescript-config",
  "version": "0.0.0",
  "private": true,
  "files": ["base.json", "nextjs.json", "react-library.json"]
}
// packages/typescript-config/base.json
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  }
}

Consumed:

// apps/web/tsconfig.json
{
  "extends": "@myorg/typescript-config/nextjs.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

55. Versioning Strategies

Strategy Description When to use
Fixed/Locked All packages share one version Small teams, tightly coupled
Independent Each package versioned separately Library publishing, large teams
0.0.0 everywhere No versioning (internal only) Private monorepos that don't publish

For publishing, use Changesets:

pnpm add -D @changesets/cli -w
pnpm changeset init
pnpm changeset        # Create a changeset
pnpm changeset version # Apply version bumps
pnpm changeset publish # Publish to npm

56. Managing Conflicting Dependency Versions

Problem: apps/web needs React 18, apps/legacy needs React 17.

Solutions:

  1. pnpm overrides: Force a single version repo-wide.
  2. pnpm catalog (v9+): Centralized dependency version management.
  3. Accept multiple versions: pnpm's strict isolation handles this naturally.
  4. Syncpack: Tool to enforce version consistency.
// Root package.json
{
  "pnpm": {
    "overrides": {
      "react": "^18.0.0"
    }
  }
}

Part VIII — TypeScript in Turborepo

57. Shared tsconfig Packages

(Covered in Section 54. Pattern: packages/typescript-config)

58. Project References vs. Workspace Compilation

Option A: Independent compilation (recommended for Turborepo) Each package compiles independently with its own tsconfig.json. Turbo orchestrates the build order via dependsOn: ["^build"].

Option B: TypeScript Project References

// tsconfig.json (root)
{
  "references": [
    { "path": "packages/utils" },
    { "path": "packages/ui" },
    { "path": "apps/web" }
  ]
}

With tsc --build. This leverages TypeScript's own incremental/composite build system. Can coexist with Turbo but adds complexity.

Recommendation: Use Turbo's task orchestration instead of TS project references. Simpler and more cacheable.

59. Composite Builds

If using project references:

// packages/utils/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "dist"
  }
}

60. Path Aliases & Module Resolution

// apps/web/tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"],
      "@myorg/ui": ["../../packages/ui/src"],
      "@myorg/utils": ["../../packages/utils/src"]
    }
  }
}

For the JIT (Just-in-Time) pattern, point paths to source files so your IDE resolves types without building.

61. Type-Checking as a Turbo Task

// turbo.json
{
  "tasks": {
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    }
  }
}
// packages/ui/package.json
{
  "scripts": {
    "typecheck": "tsc --noEmit"
  }
}

Separate type-checking from building allows caching each independently. A lint change shouldn't invalidate typecheck.


Part IX — Building, Linting & Testing Pipelines

62. Build Task Orchestration

Typical build pipeline:

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["NODE_ENV"]
    }
  }
}

Execution flow for turbo run build:

packages/typescript-config (no build script → skipped)
packages/eslint-config (no build script → skipped)
packages/utils#build    ─┐
                         ├── parallel
packages/database#build ─┘
         │
packages/ui#build (depends on utils)
         │
    ┌────┴────┐
    │         │
apps/web   apps/api     ── parallel
  #build     #build

63. Linting with Shared ESLint Configs

// turbo.json
{
  "tasks": {
    "lint": {
      "dependsOn": ["^build"],
      "inputs": [
        "src/**/*.ts",
        "src/**/*.tsx",
        ".eslintrc.*",
        "eslint.config.*",
        "../../packages/eslint-config/**"
      ],
      "outputs": []
    }
  }
}

Note: dependsOn: ["^build"] may be needed if ESLint plugins resolve types from built packages.

64. Unit Testing

{
  "tasks": {
    "test": {
      "dependsOn": ["build"],
      "inputs": [
        "src/**",
        "test/**",
        "__tests__/**",
        "vitest.config.*",
        "jest.config.*"
      ],
      "outputs": ["coverage/**"],
      "env": ["CI"]
    }
  }
}

65. End-to-End Testing

{
  "tasks": {
    "test:e2e": {
      "dependsOn": ["build"],
      "outputs": [
        "playwright-report/**",
        "test-results/**"
      ],
      "cache": false,
      "env": ["E2E_BASE_URL", "CI"]
    }
  }
}

E2E tests are often not cached because they depend on external services, browser state, etc.

66. Code Formatting

# Run prettier from root (not through turbo — it's faster)
prettier --write "**/*.{ts,tsx,json,md}"

Formatting is typically a root-level task, not per-workspace, because Prettier is fast enough to run on the entire repo and doesn't have workspace-specific config.

If you do want it per-workspace:

{
  "tasks": {
    "format": {
      "outputs": [],
      "cache": false
    }
  }
}

Part X — Development Workflow

67. Running Dev Servers in Parallel

{
  "tasks": {
    "dev": {
      "dependsOn": ["^build"],
      "persistent": true,
      "cache": false
    }
  }
}
# Start all dev servers
turbo run dev

# Start specific ones
turbo run dev --filter=web --filter=api

Turbo runs all dev servers simultaneously, each in its own process, with combined output in the terminal.

The new TUI (Terminal UI) mode ("ui": "tui") provides a split-pane interactive terminal for each persistent task.

68. Watch Mode & Rebuilds

{
  "tasks": {
    "dev": {
      "persistent": true,
      "cache": false
    }
  }
}

For packages, use the tool's built-in watch mode:

// packages/ui/package.json
{
  "scripts": {
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
  }
}

turbo watch (v2): Automatically re-runs tasks when source files change:

turbo watch build lint test

69. Hot Module Replacement Across Packages

When using the JIT pattern:

  1. App dev server (Next.js, Vite) watches source files.
  2. Package changes are picked up immediately (since they point to source).
  3. HMR fires in the app.

When building packages:

  1. Package dev script runs tsup --watch.
  2. Outputs dist/.
  3. App dev server detects dist/ change → HMR.

70. Using turbo with Docker

Naive approach (slow, large images):

COPY . .
RUN npm install
RUN npx turbo run build --filter=web

Optimized approach with turbo prune:

# Stage 1: Prune
FROM node:20-alpine AS pruner
RUN npm install -g turbo
WORKDIR /app
COPY . .
RUN turbo prune @myorg/web --docker

# Stage 2: Install
FROM node:20-alpine AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable && pnpm install --frozen-lockfile

# Stage 3: Build
COPY --from=pruner /app/out/full/ .
RUN corepack enable && pnpm turbo run build --filter=@myorg/web

# Stage 4: Run
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=installer /app/apps/web/.next/standalone ./
COPY --from=installer /app/apps/web/.next/static ./.next/static
COPY --from=installer /app/apps/web/public ./public
CMD ["node", "server.js"]

71. turbo prune for Optimized Docker Layers

The --docker flag creates two directories:

  • out/json/ — Only package.json files and the lockfile. Used for the install layer. Changes to source code don't invalidate this Docker layer.
  • out/full/ — Full source code. Used for the build layer.

This separation means pnpm install (expensive) is only re-run when dependencies change, not on every code change.

72. CI/CD Integration

GitHub Actions example:

name: CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "pnpm"

      - run: pnpm install --frozen-lockfile

      - run: pnpm turbo run build test lint
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Key optimizations:

  1. Remote caching — share cache across CI runs.
  2. Filter by change — only build/test affected packages:
    - run: pnpm turbo run build test --filter="...[origin/main...HEAD]"
  3. Cache node_modules — use actions/cache or pnpm's built-in caching.

73. Caching in CI — Remote Cache Best Practices

  1. Enable remote caching for all CI jobs.
  2. Use read-only cache in PRs to prevent cache pollution:
    TURBO_REMOTE_CACHE_READ_ONLY=true turbo run build
  3. Populate cache from main branch — CI on main pushes to cache; PRs read from it.
  4. Scope by team — all team members share one cache.
  5. Sign artifacts in sensitive environments.

74. Branch-Based & PR-Based Cache Scoping

Turbo's cache is global (not branch-scoped) — any hash match from any branch is a valid hit. This is correct because hashing is content-based.

If you need isolation:

  • Different TURBO_TEAM per environment
  • Self-hosted cache with custom scoping logic

Part XI — Advanced Patterns

75. Polyglot Monorepos

Turborepo is JavaScript-centric but can orchestrate non-JS tasks:

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["target/**"]
    }
  }
}
// packages/rust-lib/package.json
{
  "name": "@myorg/rust-lib",
  "scripts": {
    "build": "cargo build --release"
  }
}

As long as there's a package.json with scripts, Turbo can orchestrate it. The workspace doesn't have to be JavaScript.

76. Publishing Packages (Changesets)

pnpm add -D @changesets/cli -w
pnpm changeset init

Workflow:

# 1. Developer creates a changeset
pnpm changeset
# Prompts: which packages changed? major/minor/patch? description?

# 2. CI or release process versions packages
pnpm changeset version

# 3. Publish
pnpm turbo run build --filter="...[HEAD^]"
pnpm changeset publish

CI automation with GitHub Actions:

- uses: changesets/action@v1
  with:
    publish: pnpm changeset publish
    version: pnpm changeset version
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

77. Feature Flags & Environment-Specific Builds

// turbo.json
{
  "tasks": {
    "build": {
      "env": [
        "FEATURE_*",
        "NEXT_PUBLIC_FEATURE_*"
      ]
    }
  }
}

Different env var values produce different hashes → different cache entries. So building with FEATURE_NEW_UI=true caches separately from FEATURE_NEW_UI=false.

78. Micro-Frontends with Turborepo

apps/
├── shell/           # Host/shell application (Module Federation)
├── mfe-auth/        # Auth micro-frontend
├── mfe-dashboard/   # Dashboard micro-frontend
└── mfe-settings/    # Settings micro-frontend
packages/
├── shared-types/    # Shared TypeScript interfaces
└── shared-utils/    # Shared utilities

Each MFE builds independently. Turbo caches each one. Module Federation or import maps handle runtime composition.

79. Multiple Frameworks

apps/
├── marketing/     # Next.js (SSR/SSG)
├── dashboard/     # Vite + React (SPA)
├── docs/          # Astro (static)
├── api/           # Fastify
└── admin/         # Remix
packages/
├── ui/            # Framework-agnostic components
└── api-client/    # Generated API client (OpenAPI)

Each app has its own framework setup. Shared packages are framework-agnostic. Turbo orchestrates everything uniformly.

80. Dynamic Workspace Inclusion/Exclusion

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
  - "!packages/deprecated-*"   # Exclude deprecated packages

81. The Turbo Daemon

The daemon is a background process that watches the filesystem for changes. Benefits:

  • Faster hashing — incremental file watching instead of walking the entire tree.
  • Persistent state — keeps the workspace graph in memory.
// turbo.json
{
  "daemon": true  // Default in v2
}

Manage the daemon:

turbo daemon status
turbo daemon stop
turbo daemon restart

82. Codemods & Migration

# Migrate turbo.json from v1 to v2
npx @turbo/codemod migrate

# Specific transforms
npx @turbo/codemod rename-pipeline    # pipeline → tasks
npx @turbo/codemod add-package-manager
npx @turbo/codemod set-default-outputs

Part XII — Performance & Scaling

83. Benchmarks

Turborepo claims and demonstrates:

  • ~65-85% faster CI builds with remote caching
  • Millisecond replays on full cache hits
  • Linear scaling with workspace count when cache is warm

Real-world example:

Scenario Without Turbo With Turbo (cold) With Turbo (warm)
20-package build 4 min 2 min 8 sec
50-package CI 12 min 5 min 30 sec

84. Multitasking Parallelism

Turbo's scheduler:

  1. Builds the task graph.
  2. Identifies tasks with no pending dependencies.
  3. Executes them in parallel (up to --concurrency limit).
  4. As tasks complete, new tasks become unblocked.
  5. Repeats until all tasks are done.

This is optimal — it achieves maximum parallelism given the dependency constraints.

85. The Turbo Daemon (Performance)

Performance impact:

Operation Without Daemon With Daemon
Hash computation (large repo) ~2-5s ~50-200ms
Workspace discovery ~500ms ~10ms
File change detection Walk entire tree Instant (inotify/FSEvents)

86. Scaling to Hundreds of Packages

Recommendations for large monorepos:

  1. Always use remote caching.
  2. Use pnpm — fastest installs, strictest isolation.
  3. Narrow inputs — reduce hash computation.
  4. Use the daemon — faster file watching.
  5. Filter in CI — only build affected packages.
  6. Split CI jobs — parallelize across runners.
  7. Use turbo prune — for deployment builds.

87. Profiling

# Generate a profile
turbo run build --profile=profile.json

# View in Chrome DevTools (chrome://tracing)
# or https://ui.perfetto.dev

The profile shows:

  • Task execution timeline
  • Cache hit/miss per task
  • Wait times (blocked on dependencies)
  • Parallelism utilization

Part XIII — Ecosystem & Integrations

88. Vercel Deployment

Vercel has native Turborepo support:

  • Automatic remote caching (no config needed)
  • Automatic workspace detection
  • Build command: cd ../.. && npx turbo run build --filter=web
  • Root directory setting: apps/web

In Vercel project settings:

Root Directory: apps/web
Build Command: cd ../.. && pnpm turbo run build --filter=@myorg/web
Output Directory: .next
Install Command: pnpm install

89. Next.js Monorepo Patterns

// apps/web/next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  transpilePackages: ["@myorg/ui", "@myorg/utils"],
  output: "standalone",  // For Docker
  experimental: {
    outputFileTracingRoot: path.join(__dirname, "../../"),
  },
};

90. Storybook

// packages/ui/package.json
{
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build"
  }
}
// turbo.json
{
  "tasks": {
    "storybook": {
      "persistent": true,
      "cache": false
    },
    "build-storybook": {
      "dependsOn": ["^build"],
      "outputs": ["storybook-static/**"]
    }
  }
}

91. Prisma / Database Packages

packages/database/
├── prisma/
│   └── schema.prisma
├── src/
│   └── index.ts          # Re-export PrismaClient
├── package.json
└── tsconfig.json
// packages/database/package.json
{
  "name": "@myorg/database",
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "db:generate": "prisma generate",
    "db:push": "prisma db push",
    "db:migrate": "prisma migrate dev",
    "db:studio": "prisma studio"
  }
}
// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build", "db:generate"]
    },
    "db:generate": {
      "cache": false
    },
    "db:push": {
      "cache": false
    }
  }
}

92. Shared UI Component Libraries (Design Systems)

packages/ui/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   ├── Input.tsx
│   │   └── index.ts
│   ├── hooks/
│   ├── styles/
│   └── index.ts
├── package.json
└── tsconfig.json

Export pattern:

{
  "name": "@myorg/ui",
  "exports": {
    ".": "./src/index.ts",
    "./button": "./src/components/Button.tsx",
    "./card": "./src/components/Card.tsx",
    "./styles.css": "./src/styles/globals.css"
  }
}

93. API & Full-Stack App Patterns

apps/
├── web/              # Next.js frontend
├── api/              # Express/Fastify API
packages/
├── api-client/       # Type-safe API client (generated from OpenAPI or tRPC)
├── validators/       # Zod schemas shared between frontend & backend
├── types/            # Shared TypeScript interfaces
└── database/         # Prisma

With tRPC, you get end-to-end type safety:

packages/trpc/        # tRPC router definitions
apps/web/             # tRPC client (type-safe)
apps/api/             # tRPC server

Part XIV — Troubleshooting & Common Pitfalls

94. "Cache Miss" When You Expect a Hit

Diagnosis:

turbo run build --dry-run=json --filter=web

Compare the hash and inputs between runs.

Common causes:

  1. Unlisted env var changing between runs → add to env or globalEnv.
  2. Timestamp in output → make builds deterministic.
  3. Different node_modules → ensure lockfile is committed and identical.
  4. Generated file differing → add to .gitignore or inputs exclusion.
  5. OS differences → lockfile may differ between macOS/Linux.
  6. turbo.json changed → expected, the config is part of the hash.

95. Phantom Dependencies

A phantom dependency is a package used in code but not declared in package.json. It works due to hoisting but breaks in strict environments.

// packages/ui/src/index.ts
import lodash from 'lodash';  // Not in ui's package.json!
// Works because npm/yarn hoists it from another workspace

Fix: Use pnpm (strict by default) or a linting rule to catch undeclared imports.

96. Circular Dependencies

packages/a → packages/b → packages/a  ← CYCLE!

Turbo will error on circular workspace dependencies (the DAG cannot be topologically sorted).

Fix:

  1. Extract shared code into a third package.
  2. Use dependency injection.
  3. Restructure to remove the cycle.

97. Platform/OS-Specific Cache Invalidation

Problem: macOS developer pushes cache, Linux CI runner can't use it because native modules (e.g., esbuild, swc) differ.

Solution: Turbo includes platform info in the hash for tasks that depend on native binaries. The lockfile's platform-specific resolution also affects the hash.

98. node_modules Resolution Issues

Symptoms: Module not found errors despite packages being installed.

Fixes:

  1. Ensure workspace:* protocol is used for internal deps.
  2. Run pnpm install from root.
  3. Check .npmrc hoisting settings.
  4. Verify exports / main fields in package.json.
  5. Clear node_modules and reinstall: pnpm install --force.

99. Out-of-Memory & Large Monorepo Issues

# Increase Node memory
NODE_OPTIONS="--max-old-space-size=8192" turbo run build

# Reduce concurrency to limit memory
turbo run build --concurrency=2

# Use turbo prune for deployment builds
turbo prune @myorg/web --docker

Part XV — Migration & Adoption

100. Migrating from Lerna

# 1. Install turbo
pnpm add -D turbo -w

# 2. Create turbo.json matching your lerna.json tasks
# 3. Replace lerna run with turbo run in scripts/CI
# 4. Remove lerna (optional — keep for versioning/publishing)

# Keep lerna for publishing, use turbo for building:
{
  "scripts": {
    "build": "turbo run build",       // Turbo
    "publish": "lerna publish"         // Lerna
  }
}

101. Migrating from Nx

Key differences to address:

  • project.jsonpackage.json scripts + turbo.json
  • nx.json targets → turbo.json tasks
  • Nx generators → turbo gen (simpler)
  • Nx plugins → manual setup (Turbo has fewer plugins)
  • nx affectedturbo run --filter=...[main...HEAD]

102. Migrating from Polyrepo to Monorepo

  1. Create monorepo structure with apps/ and packages/.
  2. Move repos into the structure (preserving git history with git subtree or fresh start).
  3. Deduplicate dependencies — merge lockfiles, unify versions.
  4. Extract shared code into packages.
  5. Add workspace:* references.
  6. Configure turbo.json.
  7. Update CI/CD pipelines.

103. Incremental Adoption

Turbo can be added to an existing monorepo without changing anything:

# 1. Install
pnpm add -D turbo -w

# 2. Add minimal turbo.json
echo '{ "tasks": { "build": { "outputs": ["dist/**"] } } }' > turbo.json

# 3. Replace `pnpm -r run build` with `turbo run build`
# That's it! Immediate caching + parallelism benefits.

104. Turbo v1 → v2 Migration

npx @turbo/codemod migrate

Key changes:

  • pipelinetasks
  • Automatic environment variable detection improved
  • New TUI ("ui": "tui")
  • Daemon on by default
  • turbo watch command
  • Improved filtering
  • Workspace turbo.json requires "extends": ["//"]

Part XVI — Governance & Best Practices

105. Naming Conventions

Item Convention Example
Workspace names Scoped, lowercase, kebab-case @myorg/ui-components
App directories Short, descriptive apps/web, apps/api
Package directories Feature-based packages/auth, packages/ui
Config packages Suffixed with -config @myorg/eslint-config
Scripts Consistent across workspaces build, test, lint, dev

106. Ownership & CODEOWNERS

# .github/CODEOWNERS
/apps/web/              @frontend-team
/apps/api/              @backend-team
/packages/ui/           @design-system-team
/packages/database/     @backend-team
/turbo.json             @platform-team
/.github/               @platform-team

107. Monorepo Boundary Enforcement

Tools to enforce architectural boundaries:

  • eslint-plugin-import — restrict imports between packages.
  • Sherif — monorepo-specific linting (dependency consistency, etc.).
  • depcheck — find unused dependencies.
  • syncpack — enforce consistent dependency versions.
  • publint — validate package.json for publishing.

Custom ESLint rule example:

// Don't allow apps to import from other apps
"import/no-restricted-paths": [{
  zones: [{
    target: "./apps/web",
    from: "./apps/api"
  }]
}]

108. Documentation Strategies

docs/
├── architecture.md          # Monorepo architecture overview
├── adding-a-package.md      # How to create new packages
├── dependency-policy.md     # Rules about dependencies
└── ci-cd.md                 # CI/CD pipeline documentation

packages/ui/
└── README.md                # Package-specific docs

Additionally, use turbo gen to codify patterns — new package creation becomes a guided, automated process.

109. When NOT to Use a Monorepo / Turborepo

Don't use a monorepo when:

  • Teams have completely separate tech stacks with no shared code.
  • Strict repository-level access control is required (per-project permissions).
  • The projects are maintained by entirely different organizations.
  • Git performance is critical and the repo would exceed millions of files.

Don't use Turborepo specifically when:

  • You need distributed task execution across multiple machines (use Nx or Bazel).
  • You need deep polyglot support (Go, Rust, Java alongside JS — use Bazel).
  • You need fine-grained project graph plugins (use Nx).
  • Your monorepo is < 2 packages with no shared code (overkill).

Quick Reference Cheat Sheet

# Setup
npx create-turbo@latest                    # Create new monorepo
pnpm add -D turbo -w                       # Add to existing

# Running
turbo run build                            # Build everything
turbo run build test lint                   # Multiple tasks
turbo run build --filter=web               # Single workspace
turbo run build --filter=web...            # Workspace + deps
turbo run build --filter=...[main...HEAD]  # Changed since main

# Cache
turbo run build --force                    # Skip cache
turbo run build --dry-run                  # Preview without running
turbo run build --summarize                # Generate run summary

# Remote cache
turbo login && turbo link                  # Enable remote caching

# Docker
turbo prune @myorg/web --docker            # Pruned output for Docker

# Debugging
turbo run build --graph                    # Visualize task graph
turbo run build --dry-run=json             # Inspect hashes
turbo run build --verbosity=2              # Verbose output

# Maintenance
turbo daemon status                        # Check daemon
npx @turbo/codemod migrate                # Migrate to latest

This guide covers Turborepo comprehensively from first principles through advanced production patterns. Each section builds on the previous, creating a complete mental model for building, scaling, and maintaining a Turborepo-based monorepo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment