turborepo

Turborepo monorepo build system guidance. Triggers on: turbo.json, task pipelines, dependsOn, caching, remote cache, the "turbo" CLI, --filter, --affected, CI optimization, environment variables, internal packages, monorepo structure/best practices, and boundaries. Use when user: configures tasks/workflows/pipelines, creates packages, sets up monorepo, shares code between apps, runs changed/affected packages, debugs cache, or has apps/packages directories.

Install
npx skills add 'https://github.com/vercel/turborepo/tree/main/skills/turborepo'
Download bundle ↓
main · 064d74aScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md

Environment Variable Gotchas

Common mistakes and how to fix them.

.env Files Must Be in inputs

Turbo does NOT read .env files. Your framework (Next.js, Vite, etc.) or dotenv loads them. But Turbo needs to know when they change.

Wrong:

{
  "tasks": {
    "build": {
      "env": ["DATABASE_URL"]
    }
  }
}

Right:

{
  "tasks": {
    "build": {
      "env": ["DATABASE_URL"],
      "inputs": ["$TURBO_DEFAULT$", ".env", ".env.local", ".env.production"]
    }
  }
}

Strict Mode Filters CI Variables

In strict mode, CI provider variables (GITHUB_TOKEN, GITLAB_CI, etc.) are filtered unless explicitly listed.

Symptom: Task fails with "authentication required" or "permission denied" in CI.

Solution:

{
  "globalPassThroughEnv": ["GITHUB_TOKEN", "GITLAB_CI", "CI"]
}

passThroughEnv Doesn't Affect Hash

Variables in passThroughEnv are available at runtime but changes WON'T trigger rebuilds.

Dangerous example:

{
  "tasks": {
    "build": {
      "passThroughEnv": ["API_URL"]
    }
  }
}

If API_URL changes from staging to production, Turbo may serve a cached build pointing to the wrong API.

Use passThroughEnv only for:

  • Auth tokens that don't affect output (SENTRY_AUTH_TOKEN)
  • CI metadata (GITHUB_RUN_ID)
  • Variables consumed after build (deploy credentials)

Runtime-Created Variables Are Invisible

Turbo captures env vars at startup. Variables created during execution aren't seen.

Won't work:

# In package.json scripts
"build": "export API_URL=$COMPUTED_VALUE && next build"

Solution: Set vars before invoking turbo:

API_URL=$COMPUTED_VALUE turbo run build

Different .env Files for Different Environments

If you use .env.development and .env.production, both should be in inputs.

{
  "tasks": {
    "build": {
      "inputs": [
        "$TURBO_DEFAULT$",
        ".env",
        ".env.local",
        ".env.development",
        ".env.development.local",
        ".env.production",
        ".env.production.local"
      ]
    }
  }
}

Complete Next.js Example

{
  "$schema": "https://v2-10-14-canary-1.turborepo.dev/schema.json",
  "globalEnv": ["CI", "NODE_ENV", "VERCEL"],
  "globalPassThroughEnv": ["GITHUB_TOKEN", "VERCEL_URL"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "env": ["DATABASE_URL", "NEXT_PUBLIC_*", "!NEXT_PUBLIC_ANALYTICS_ID"],
      "passThroughEnv": ["SENTRY_AUTH_TOKEN"],
      "inputs": [
        "$TURBO_DEFAULT$",
        ".env",
        ".env.local",
        ".env.production",
        ".env.production.local"
      ],
      "outputs": [".next/**", "!.next/cache/**", "!.next/dev/**"]
    }
  }
}

This config:

  • Hashes DATABASEURL and NEXT_PUBLIC* vars (except analytics)
  • Passes through SENTRY_AUTH_TOKEN without hashing
  • Includes all .env file variants in the hash
  • Makes CI tokens available globally

With futureFlags.globalConfiguration

The same config using the global key. The .env files move to global.inputs, which means they get folded into each task's hash individually rather than the global hash. This lets tasks exclude specific .env files if needed.

{
  "$schema": "https://v2-10-14-canary-1.turborepo.dev/schema.json",
  "futureFlags": { "globalConfiguration": true },
  "global": {
    "env": ["CI", "NODE_ENV", "VERCEL"],
    "passThroughEnv": ["GITHUB_TOKEN", "VERCEL_URL"],
    "inputs": [".env", ".env.local", ".env.production", ".env.production.local"]
  },
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "env": ["DATABASE_URL", "NEXT_PUBLIC_*", "!NEXT_PUBLIC_ANALYTICS_ID"],
      "passThroughEnv": ["SENTRY_AUTH_TOKEN"],
      "outputs": [".next/**", "!.next/cache/**", "!.next/dev/**"]
    }
  }
}

With this approach, a task that doesn't care about .env.production can exclude it:

"lint": {
  "inputs": ["$TURBO_DEFAULT$", "!$TURBO_ROOT$/.env.production"]
}

This wouldn't have been possible with globalDependencies, where .env.production would be baked into the global hash and affect every task unconditionally.

Referenced from SKILL.md