> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Turborepo usage

> Status: Active Owner: Platform Architecture Phase: 1.A of docs/recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md

**Status:** Active
**Owner:** Platform Architecture
**Phase:** 1.A of [`docs/recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md`](../recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md)

This repo is wired to [Turborepo](https://turborepo.com/) with **npm workspaces** (overlay packages under `packages/*` — see [ADR-016](../architecture/decisions/ADR-016-workspace-packages-overlay.md)). Turborepo provides content-addressed caching for `typecheck`, `lint:ci`, `format:check`, `test:unit`, `test:integration`, and `build`. When inputs do not change, the cached result is reused — locally and (when Remote Cache secrets are set) across CI runners.

Adopting Turborepo does **not** change the primary Vite application layout: root `package.json`, `vite.config.ts`, and the main Vercel project remain canonical. Turborepo wraps the existing npm scripts and accelerates CI/local iteration. **Turborepo is not Turbopack** (Next.js’s bundler); see [ADR-019](../architecture/decisions/ADR-019-nextjs-turbopack-deferral.md).

## Quick reference

```bash theme={null}
# Cached single-tasks (replace plain npm equivalents when you want cache reuse)
npm run turbo:typecheck      # = turbo run typecheck       (~9 min cold, ~350 ms cache hit)
npm run turbo:lint           # = turbo run lint:ci         (~88 s cold, ~5 s cache hit)
npm run turbo:format:check   # = turbo run format:check
npm run turbo:test:unit      # = turbo run test:unit       (~3.7 min cold)
npm run turbo:build          # = turbo run build           (depends on //#typecheck in turbo.json)
npm run turbo:validate       # = turbo run format:check typecheck lint:ci build (same as npm run validate)

# Inspect cache state without running anything
npm run turbo:cache:status

# Force a re-run, ignoring cache (useful when debugging cache-key mistakes)
npx turbo run typecheck --force

# See what tasks would run, with hashes
npx turbo run typecheck --dry-run
```

The plain `npm run typecheck` / `npm run lint:ci` / `npm run format:check` / `npm run build` scripts still exist for direct runs. **`npm run validate`** orchestrates the four gates through Turborepo (same graph as `npm run turbo:validate`). Use plain scripts when you want a single step without the full graph (e.g., `typecheck` only for a baseline measurement).

## Why Turborepo here

Today the repo is **8,282 TS/TSX files**, **859 SQL migrations**, **355 edge functions**, **1,657 test files** (per `reports/perf/baseline-2026-04-22.md`). Every PR that doesn't touch `src/` still pays the full cost of typecheck, lint, format check, unit tests. Turborepo caches each of those by content hash of declared inputs, so:

* A PR that only changes files outside task `inputs` (e.g. many `specs/` or `docs/` markdown-only edits) can see **format/typecheck/lint/build cache hits** and finish much faster once caches are warm.
* A PR touching only `src/cores/hr/*` invalidates `typecheck`, `lint:ci`, `test:unit`, `build` — but not `format:check` (different input set).
* The cache key includes `tsconfig*.json`, `package-lock.json`, the relevant config files (`vite.config.ts`, `vitest.config.ts`, `biome.json`, `eslint.config.js`), and the input files themselves.

The wins compound when per-package `typecheck` / `build` scripts exist on workspace packages — at that point `turbo run typecheck --filter=...[origin/prod]` can narrow work to affected subgraphs. Today, root `//#` tasks compile the full `src/` tree (see `turbo.json`).

## Configuration

See [`turbo.json`](../../turbo.json). Key decisions:

* **`globalDependencies`** lists tracked config files that, if changed, must invalidate every cache entry (`tsconfig*.json`, `vite.config.ts`, `vitest.config.ts`, `biome.json`, `eslint.config.js`, `package*.json`). **`.env` / `.env.*` are not global deps** — they are listed on the **`//#build`** task `inputs` so local env churn does not bust typecheck/lint/format caches. If a change should invalidate every task, add it here deliberately.
* **`globalEnv`** lists env vars whose values affect every task (`NODE_ENV`, `CI`, `VERCEL_PROJECT_ID`).
* Each task declares its **`inputs`** explicitly. We do not use the default "everything in the workspace" input set — that would cause spurious cache invalidations on docs/spec edits.
* `//#build` declares `dependsOn: ["//#typecheck"]` so `turbo run build` always waits on typecheck; a cache hit on typecheck satisfies the dependency without re-running it.
* `dev` is `cache: false, persistent: true`.

### Sandboxing caveats

Turborepo 2.x has **no task sandboxing** — tasks can read files outside their declared `inputs`. If a script secretly reads (for example) `docs/` or `specs/`, the cache will return false hits when those files change. Mitigations:

1. Run `npm run turbo:cache:status` before merging PRs that change input declarations to confirm hashes change as expected.
2. When in doubt, `npx turbo run <task> --force` to bypass cache.
3. If we hit recurring cache poisoning, that is the trigger to revisit Nx (which provides task sandboxing).

## Vercel Remote Cache (optional; recommended)

**Local `.turbo/`** is already restored across GitHub Actions runs via `actions/cache` on `.turbo/`. **Vercel Remote Cache** adds cross-machine reuse (developers ↔ CI ↔ preview) when credentials exist.

1. In the Vercel dashboard, open each project that runs Turborepo (`encoreos_dev`, `encoreos_prod`, and any future `encoreos-public` project).
2. Project Settings → General → "Build & Development Settings" → ensure "Remote Cache" is enabled (on by default for eligible plans).
3. Locally: `npx turbo login` then `npx turbo link` once per workstation (token in `~/.turbo/config.json`).
4. In GitHub: add repository secrets `TURBO_TOKEN` and `TURBO_TEAM` (or `TURBO_TEAM` as a **variable** if you prefer non-secret team slug). `.github/workflows/build.yml` passes them as job-level `env` so every `npx turbo run …` step can upload/download remote artifacts.
5. **Verify cache hits:** open a PR, push an empty commit, and compare Turbo output on the second run — or run locally:
   ```bash theme={null}
   npx turbo run typecheck --summarize
   ```
   Remote hits show as remote cache reads in the summary (exact wording varies by Turbo version).

If `TURBO_TOKEN` is **unset**, Turborepo still works; CI relies on the Actions cache of `.turbo/` plus per-task input hashing.

Docs: [https://vercel.com/docs/monorepos/remote-caching](https://vercel.com/docs/monorepos/remote-caching)

## Wiring into CI

`.github/workflows/build.yml` runs **`Typecheck`**, **`Lint`**, and **`Build`** through Turborepo (`npx turbo run typecheck` / `lint:ci` / `build`). The `build` job sets `TURBO_TOKEN` / `TURBO_TEAM` from secrets (see above). A GitHub Actions cache step restores `.turbo/` across runs:

```yaml theme={null}
- name: Cache Turborepo local cache
  uses: actions/cache@v5
  with:
    path: .turbo
    key: turbo-${{ runner.os }}-${{ github.sha }}
    restore-keys: |
      turbo-${{ runner.os }}-

- name: Typecheck via Turborepo
  run: npx turbo run typecheck

- name: Lint via Turborepo
  run: npx turbo run lint:ci

- name: Build via Turborepo
  run: npx turbo run build
```

`Format check (Biome)` is intentionally **NOT** wrapped in Turborepo — it stays as the plain `npm run format:check` because the codebase has known pre-existing format issues (per `AGENTS.md` § Cursor Cloud caveats), and we want zero behavior change in this CI-wiring PR. A future PR can switch it once those are resolved.

`Unit and integration tests` (the `test:coverage` step) also stays on plain npm because Turborepo correctly does not cache failed test runs — the codebase has 2 pre-existing test failures (per `AGENTS.md`) which would mean every PR re-runs the suite from scratch anyway. Once those are resolved, switching to `npx turbo run test:unit` will start producing cache hits.

### Vercel Remote Cache in CI

The build workflow sets job-level `env` for `TURBO_TOKEN` / `TURBO_TEAM` when secrets exist. A diagnostic step prints whether the token is present (never logs the token). See the workflow file and the section above for setup.

## Measured wins on this branch

Captured on the Cursor Cloud VM (Linux, 4 vCPU / 16 GB RAM):

| Task                  | Cold (cache miss) | Warm (cache hit)         | Speedup |
| --------------------- | ----------------- | ------------------------ | ------- |
| `turbo run typecheck` | **10 m 49 s**     | **345 ms** (FULL TURBO)  | \~1880× |
| `turbo run lint:ci`   | **1 m 28 s**      | **5.3 s** (ESLint cache) | \~17×   |

Typecheck is the dominant pole. Cache hits like the one above are exactly what makes a "no-source-change" PR (docs, spec, scripts/) finish CI in seconds.

## Troubleshooting

| Symptom                                                        | Likely cause                                                          | Fix                                                                                                                                                        |
| -------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "Could not resolve workspaces. Missing `packageManager` field" | Older `package.json` without `packageManager`                         | Already fixed; `packageManager: "npm@..."` is set.                                                                                                         |
| Cache miss when you expected a hit                             | An input you didn't declare changed                                   | `npx turbo run <task> --dry-run --output-logs=full` shows the resolved input set per task.                                                                 |
| Cache hit when you expected a miss (wrong result returned)     | An undeclared input was modified                                      | Add it to the task's `inputs` (or to `globalDependencies` if it affects every task).                                                                       |
| Vite bundle wrong after only editing `.env.local`              | Build cache hit without env in fingerprint                            | `.env*` paths are on `//#build` `inputs`; run `npx turbo run build --force` if you suspect a stale env fingerprint.                                        |
| `lint:ci` cache never hits                                     | Lint exits non-zero (25 pre-existing errors on `prod`)                | Expected; Turbo correctly does not cache failed runs. ESLint's own `--cache` still helps within a single run.                                              |
| Local cache out of date / wrong                                | Bust it                                                               | `rm -rf .turbo node_modules/.cache && npx turbo run typecheck --force`                                                                                     |
| CI never shows remote cache hits                               | `TURBO_TOKEN` / `TURBO_TEAM` unset, wrong team slug, or token revoked | Re-create secrets from `turbo login` / Vercel; confirm team slug matches `turbo link` / dashboard.                                                         |
| `Unable to find packages referenced in microfrontends.json`    | Application keys are not npm workspace package names                  | Ensure each key matches a `packages/*/package.json` `name` (see `packages/encoreos-app` / `packages/encoreos-public`). Run `npm run audit:microfrontends`. |

## See also

* [`turbo.json`](../../turbo.json) — task definitions
* [`docs/recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md`](../recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md) — Phase 1 plan
* [Turborepo docs](https://turborepo.com/docs)
* [Vercel Remote Cache docs](https://vercel.com/docs/monorepos/remote-caching)
