Skip to main content

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.

Status: Active Owner: Platform Architecture Phase: 1.A of docs/recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md This repo is wired to Turborepo with npm workspaces (overlay packages under packages/* — see ADR-016). 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.

Quick reference

# 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. 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).
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:
    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

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:
- 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):
TaskCold (cache miss)Warm (cache hit)Speedup
turbo run typecheck10 m 49 s345 ms (FULL TURBO)~1880×
turbo run lint:ci1 m 28 s5.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

SymptomLikely causeFix
”Could not resolve workspaces. Missing packageManager field”Older package.json without packageManagerAlready fixed; packageManager: "npm@..." is set.
Cache miss when you expected a hitAn input you didn’t declare changednpx 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 modifiedAdd it to the task’s inputs (or to globalDependencies if it affects every task).
Vite bundle wrong after only editing .env.localBuild 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 hitsLint 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 / wrongBust itrm -rf .turbo node_modules/.cache && npx turbo run typecheck --force
CI never shows remote cache hitsTURBO_TOKEN / TURBO_TEAM unset, wrong team slug, or token revokedRe-create secrets from turbo login / Vercel; confirm team slug matches turbo link / dashboard.
Unable to find packages referenced in microfrontends.jsonApplication keys are not npm workspace package namesEnsure each key matches a packages/*/package.json name (see packages/encoreos-app / packages/encoreos-public). Run npm run audit:microfrontends.

See also