Turborepo is a high-performance build system for JavaScript and TypeScript monorepos. A monorepo holds multiple packages and applications in a single repository — shared component libraries, API servers, web apps, CLI tools, and configuration packages all live side by side. The advantage is that cross-cutting changes (updating a shared type, fixing a utility function, bumping a dependency) happen in a single commit across all consumers. The disadvantage is that building everything from scratch on every change is slow. Turborepo solves this by understanding the dependency graph between packages and running only the tasks that are affected by a given change, caching the rest.
The platform uses Turborepo to manage a monorepo containing multiple Next.js applications (the main storefront, the admin dashboard, the Shopify embedded app), shared packages (UI component library, API client, TypeScript configuration, ESLint configuration), and tooling scripts. Without Turborepo, running npm run build at the root would build every package sequentially, taking minutes even for a one-line change. With Turborepo, unchanged packages are served from cache, and dependent packages are built in parallel based on the topological sort of the dependency graph.
Turborepo operates on top of the existing npm/pnpm/yarn workspace protocol. It doesn’t replace the package manager — it orchestrates the execution of scripts defined in each workspace’s package.json. The configuration lives in a single turbo.json file at the repository root, which defines task pipelines, their dependencies, and caching behavior. This separation means teams can adopt Turborepo incrementally: existing scripts and workflows continue to work, and Turborepo adds caching and parallelism on top.
Turborepo constructs a directed acyclic graph (DAG) of tasks based on the dependsOn declarations in turbo.json. If the build task in package A depends on build in package B (because A imports from B), Turborepo ensures B builds first. Tasks with no dependencies run in parallel. This topological execution means the build system uses all available CPU cores while respecting dependency ordering — the maximum parallelism the dependency graph allows.
After a task completes, Turborepo hashes the inputs (source files, environment variables, dependent package outputs) and stores the outputs (compiled files, logs) in a content-addressable cache. On subsequent runs, if the input hash matches a cache entry, Turborepo replays the cached output instead of re-running the task. This transforms build times from O(total packages) to O(changed packages). The cache is local by default (.turbo/ directory) but can be shared across CI machines via Remote Caching.
Local caching helps individual developers, but CI pipelines start from a clean checkout. Remote Caching stores task outputs on a shared server (Vercel’s hosted cache, or a self-hosted S3-compatible backend). When a CI job runs a task whose inputs match a remote cache entry, it downloads the output instead of rebuilding. This means a build that took 10 minutes from scratch completes in seconds if another developer (or a previous CI run) already built the same code. The cache key includes file hashes, environment variables, and the task configuration itself.
The --filter flag lets developers scope Turborepo commands to a subset of the monorepo. turbo build --filter=@acme/web builds only the web app and its transitive dependencies. turbo build --filter=...[HEAD~1] builds only packages that changed since the last commit. This is essential for large monorepos where most developers work in a narrow slice of the codebase and don’t want to wait for unrelated packages to build.
Tasks often depend on environment variables (API URLs, feature flags, build modes). Turborepo includes specified environment variables in the cache hash via the env and globalEnv fields in turbo.json. Changing NEXT_PUBLIC_API_URL from staging to production produces a different hash, preventing a staging build from being served as a production build. Variables not listed in these fields are invisible to the cache — a common source of bugs when adopting Turborepo.
A typical Turborepo monorepo uses pnpm workspaces (or npm/yarn) to define packages. The root package.json declares workspace paths, and each workspace has its own package.json with dependencies that can reference other workspaces.
myrepo/
├── turbo.json # Turborepo pipeline config
├── package.json # Root package.json with workspace declaration
├── pnpm-workspace.yaml # pnpm workspace definition
├── apps/
│ ├── web/ # Next.js storefront
│ │ ├── package.json # depends on @acme/ui, @acme/api-client
│ │ └── src/
│ ├── admin/ # Next.js admin dashboard
│ │ ├── package.json # depends on @acme/ui, @acme/api-client
│ │ └── src/
│ └── shopify/ # Shopify embedded app
│ ├── package.json # depends on @acme/ui
│ └── src/
├── packages/
│ ├── ui/ # Shared React component library
│ │ ├── package.json # name: @acme/ui
│ │ └── src/
│ ├── api-client/ # Typed API client
│ │ ├── package.json # name: @acme/api-client
│ │ └── src/
│ ├── tsconfig/ # Shared TypeScript configuration
│ │ ├── package.json # name: @acme/tsconfig
│ │ └── base.json
│ └── eslint-config/ # Shared ESLint rules
│ ├── package.json # name: @acme/eslint-config
│ └── index.js
└── .turbo/ # Local cache directory (gitignored)
The turbo.json file defines how tasks relate to each other. The dependsOn field creates edges in the task graph. The caret prefix (^build) means “the build task in my dependencies”, while a plain name (lint) means “this task in the same package”.
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["NODE_ENV"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json", "package.json"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_SITE_URL"]
},
"lint": {
"dependsOn": ["^build"],
"inputs": ["src/**", ".eslintrc.*", "tsconfig.json"],
"outputs": []
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**", "tests/**", "vitest.config.*"],
"outputs": ["coverage/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"type-check": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
"outputs": []
}
}
}
Developers use filters to build only what they need. The filter syntax supports package names, directory paths, and git-based change detection.
# Build only the web app and its dependencies
turbo build --filter=@acme/web
# Build everything that changed since the last commit
turbo build --filter=...[HEAD~1]
# Run tests only in packages under apps/
turbo test --filter=./apps/*
# Lint only the UI package and things that depend on it
turbo lint --filter=@acme/ui...
# Build the admin app excluding the shopify app
turbo build --filter=@acme/admin --filter=!@acme/shopify
# Dry run to see what would execute without running it
turbo build --filter=@acme/web --dry-run
In the platform’s monorepo, Turborepo eliminates the “rebuild the world” problem. When a developer changes a component in @acme/ui, only the UI package rebuilds, followed by the apps that import it. Packages that don’t depend on the UI library (like the API client) are served from cache. A full rebuild that took 4 minutes without Turborepo now completes in under 30 seconds when only one package changed.
CI pipelines benefit the most from Remote Caching. The platform connects Turborepo to Vercel’s remote cache. When a pull request is opened, the CI job runs turbo build test lint. If the PR only changes the admin app, packages like @acme/ui and @acme/api-client are served from the remote cache (populated by a previous CI run on main). The CI job goes from 8 minutes to under 2 minutes for scoped changes.
The development server uses turbo dev --filter=@acme/web to start only the web app and watch its dependencies for changes. If a developer modifies a component in @acme/ui, the UI package rebuilds and the web app’s dev server picks up the change via hot module replacement. This creates a seamless cross-package development experience without manually running build watchers in multiple terminals.
Shared packages in a Turborepo monorepo are typically “internal packages” — they’re consumed only by other packages in the same repo, never published to npm. These packages can export TypeScript source directly (no build step) by setting "main": "./src/index.ts" in their package.json. The consuming app’s bundler (Next.js, Vite) transpiles the source. This eliminates the need for a separate build step for shared packages and makes changes instantly visible during development.
TSConfig, ESLint, and Prettier configurations live in dedicated packages (@acme/tsconfig, @acme/eslint-config). Apps extend these via "extends": "@acme/tsconfig/nextjs.json". This ensures consistent tooling across all packages while allowing per-package overrides. Turborepo includes these configuration files in cache hashes, so changing a shared ESLint rule correctly invalidates linting for all dependent packages.
Different apps need different environment variables. The turbo.json env field can be set per-task, but for per-app scoping, each app’s package.json can declare a turbo configuration that extends the root. Alternatively, use TURBO_CI_VENDOR_ENV_KEY prefixes to automatically include framework-specific environment variables (like NEXT_PUBLIC_* for Next.js apps).
env or globalEnv, Turborepo won’t include it in the cache hash. A staging build cached on CI will be served as a production build if the only difference is an unlisted environment variable. Always audit turbo.json for completeness and use turbo build --summarize to inspect what’s included in the hash."cache": false. A cached deploy task would silently skip the actual deployment, reporting success without doing anything. Only cache tasks whose outputs are purely local files.!.next/cache/** in outputs: Next.js’s build cache (.next/cache) is managed by Next.js itself and should not be included in Turborepo’s cache outputs. Including it bloats the cache, slows uploads/downloads, and can cause stale build artifacts. Always exclude it with the negation pattern.turbo inside a workspace directory: Turborepo should be invoked from the repository root, not from inside a workspace. Running turbo build from apps/web/ can miss the root turbo.json and produce incorrect results. Use --filter from the root instead.