Skip to content

Shared patterns with Turborepo and Nx

The companion to comparison.md. That doc lists what Turbo / Nx do that vx doesn’t. This one lists what we deliberately inherited or independently converged on — the parts a Turbo or Nx user should find familiar.

Every row cites the vx source file that implements the pattern; where a comment on the line names the upstream tool, that’s the explicit parity claim.

vx’s overall shape is Turborepo with three swaps and a few subtractions:

  • per-package config (Turbo’s turbo.json → vx’s vx.config.ts),
  • opt-in cache keyed by inputs + env + upstream + lockfile,
  • shell-only task contract,
  • tar artifact + Turbo /v8/artifacts/ remote wire,
  • topological scheduler with bounded parallelism.

The swaps: TypeScript config instead of JSON, resolved-config hash instead of file hash, strict output ownership instead of additive restore. Everything else lines up with Turbo or Nx by design.

The set of bytes fed into the hash mirrors Turbo’s key recipe with one Nx-borrowed extension.

ComponentTurboNxvxvx source
Task identity (project#task)yesyesyessrc/cache/cache.ts:25 (CacheKeyInput.taskId)
Resolved command + env declarationsyesyesyessrc/cache/cache.ts:33 (taskConfigHash)
Declared input file contentsyesyesyessrc/cache/cache.ts:40 (inputFiles)
Declared env-var values at runtimeyesyesyessrc/cache/cache.ts:38 (envValues)
Upstream task hashes (cascade)yesyesyessrc/cache/cache.ts:43 (upstreamHashes)
Workspace lockfile fingerprintyesyesyessrc/cache/cache.ts:49 (workspaceFingerprint)
Forwarded CLI args (after --)yesyesyessrc/cache/cache.ts:55 (forwardArgs)
Project package.json bytes (direct)via lockfileexternalDependenciesdirectsrc/cache/cache.ts:58 (“Turbo / Nx parity”)

The package.json fold is the Nx-borrowed move (Turbo gets it transitively through the lockfile; Nx’s externalDependencies does it explicitly). We fold the bytes directly so narrow cache.inputs.files like ['src/**'] doesn’t miss a package.json dep bump.

The two divergences from Turbo’s recipe — TypeScript-resolved config hash and per-task forwarded-args fold — are documented in caching.md and listed under “Where vx is ahead” in comparison.md.

PatternSourcevx source
Defer to git ls-files for tracked + untracked-but-not-ignoredTurbo + Nxsrc/cache/inputs.ts:214 (“Turbo / Nx model”)
Project-boundary enforcement (no cross-project globs)Turbo + Nxsrc/workspace/nested-dirs.ts
Fallback walker when git not presentTurbosrc/cache/inputs.ts

The string DSL is verbatim Turbo (Nx supports the same forms plus a richer object form).

FormMeaningSourcevx source
'lint'Same project, other taskTurbo + Nxsrc/graph/dependency-spec.ts:1 (“Turbo/Nx-style”)
'^lint'Same task in workspace dependenciesTurbo + Nxsrc/graph/dependency-spec.ts
'pkg#lint'Arbitrary other package’s taskTurbo + Nxsrc/graph/dependency-spec.ts
'*' / '^*'Wildcard upstream (filter-only)Turbo’s $TURBO_DEFAULT$-adjacentsrc/orchestrator/upstream.ts:13
'!<form>'Negation (filter-only)Turbo inputs exclusionsrc/orchestrator/upstream.ts:13

Loader-side validation rejects wildcards / negation in dependsOn itself — they’re filter-only — and the error message echoes the Turbo/Nx vocabulary: src/workspace/project-loader.ts:142.

CapabilitySourcevx source
pnpm-style --filter (pkg, pkg..., ...pkg, path globs)Turbo + pnpmsrc/workspace/filter.ts
Transitive-dep expansion (pkg...)Turbo + pnpmsrc/workspace/filter.ts:11 (“Turbo-style”)
[<since>] git-relative selectionTurbosrc/workspace/affected.ts:10 (“Matches Turbo’s [<since>]”)
--affected[=<base>] subcommandTurbo + Nxsrc/workspace/affected.ts
pkg#task direct addressingTurbo + Nxsrc/cli/run.ts
CapabilitySourcevx source
Read pnpm-workspace.yamlpnpm + Turbosrc/workspace/workspace.ts
Read package.json workspaces (npm/yarn/bun)Turbosrc/workspace/workspace.ts
Build package-dep graph from workspace dependenciesTurbo + Nxsrc/workspace/package-graph.ts
Lockfile fingerprint at workspace levelTurbo + Nxsrc/workspace/fingerprint.ts
LayerTurboNxvxvx source
Local: per-hash directorytarball-per-hash in .turbo/cache.nx/cache SQLiteSQLite + on-disk under .vx/cache/<hash>/src/cache/cache.ts
Local: hardlink-based restoreyesyesyessrc/cache/cache.ts:813 (“Same shape Turbo / Nx use”)
Read-through then write-through layeringyesyesyessrc/cache/layered-cache.ts
Run-history table for analytics(no — --summarize JSON)(Nx Cloud)runs table in cache.dbsrc/cache/cache.ts

We speak Turbo’s wire verbatim, so any compatible cache server (the ones below) works without a shim.

Endpoint / headerTurbo specvx source
HEAD/GET/PUT /v8/artifacts/<hash>?teamId=&slug=vercel/turboreposrc/cache/remote-cache.ts:8-10
POST /v8/artifacts batch existencevercel/turboreposrc/cache/remote-cache.ts:11
x-artifact-duration request headervercel/turboreposrc/cache/remote-cache.ts:30, 78
x-artifact-tag HMAC header (request + response)vercel/turboreposrc/cache/remote-cache.ts:32, 80
x-artifact-client-ci, …interactivevercel/turboreposrc/cache/remote-cache.ts:34, 36, 81
Tenant params teamId= / slug=vercel/turboreposrc/cache/remote-cache.ts:22

Compatibility matrix (verified working as of v15): ducktors/turborepo-remote-cache, Fox32/openturbo-remote-cache, Vercel hosted Turborepo cache.

Inside the tar we depart from Turbo (we don’t mimic Turbo’s .turbo/turbo-<task>-<project>.log file naming — our metadata lives in SQLite). But the on-the-wire framing stays POSIX-tar so any Turbo-aware cache server can transit our blobs unchanged.

PatternSourcevx source
POSIX ustar headers (no PAX)Turbo (Rust tar crate, Header::new_gnu())src/cache/cache.ts:831 (tar --format=ustar)
No AppleDouble ._* companionsTurbo (in-process writer, no recurse)src/cache/cache.ts:831 (COPYFILE_DISABLE=1)
Extract-side filter strips legacy PAX + ._*(vx-only defense-in-depth)src/cache/tar.ts:129-153
zstd compression on the wireTurbosrc/cache/cache.ts (tar.zst artifacts)
PatternSourcevx source
Topological order, bounded parallelismTurbo + Nxsrc/graph/scheduler.ts
Cascade abort: failed task’s transitive dependents abortTurbo (mid-mode) + Nxsrc/graph/scheduler.ts
Independent siblings continue past failureTurbo --continue=continue-tasks-with-no-depssrc/graph/scheduler.ts
Persistent / long-running tasks (dev servers)Turbo persistent, Nx continuoussrc/exec/runner.ts (runPersistent) + src/orchestrator/execute-task.ts
Project-local node_modules/.bin on PATHTurbo + pnpmsrc/orchestrator/execute-task.ts
Implicit project-package.json invalidationTurbo (via lockfile) + Nx (externalDependencies)src/orchestrator/execute-task.ts:428 (“Matches Turbo and Nx’s implicit dependencies”)
PatternSourcevx source
Glob-based outputs declarationTurbo + Nxsrc/config.ts, src/cache/inputs.ts
Restore by file (not symlink), preserve mtimeTurbo + Nxsrc/cache/cache.ts
Hardlinks point into cache; tree is read-only by conventionTurbosrc/cache/cache.ts:813-831
Log replay on cache hitTurbo + Nxsrc/orchestrator/execute-task.ts
Wipe outputs before exec AND before restore (strict ownership)(vx-only)src/cache/inputs.ts (cleanOutputs)
Flag / behaviorSourcevx source
-- separator forwards args to taskTurbosrc/cli/run.ts
--filter DSL (pnpm shape)Turbo + pnpmsrc/workspace/filter.ts
--concurrency <n>Turbosrc/cli/run.ts
--no-cache + --force synonymsTurbosrc/cli/run.ts
--dry / --dry=json (plan output)Turbosrc/orchestrator/plan.ts
--graph[=<path>] (DOT)Turbo + Nxsrc/cli/plan-format.ts
--summarize[=<path>] (per-run JSON)Turbosrc/orchestrator/run-artifacts.ts
--profile[=<path>] (Chrome-trace)Turbosrc/orchestrator/run-artifacts.ts
--affected[=<base>]Turbo + Nxsrc/workspace/affected.ts
watch <task> subcommandTurbo + Nxsrc/cli/watch.ts
PatternSourcevx source
Framed per-task blocks (┌─ task ─┐)Turbosrc/orchestrator/framed-output.ts:1, 18 (“Turbo-style header”, “matches Turbo’s”)
End-of-run summary (Tasks / Cached / Time)Turbosrc/orchestrator/summary.ts:1 (“Turbo-style end-of-run summary”)
>>> FULL TURBO-style 100%-cached bannerTurbosrc/orchestrator/summary.ts:36 (Mirrors Turbo's >>> FULL TURBO)
Per-task output buffered until task finishesTurbosrc/orchestrator/logger.ts:37 (“same as Turbo”)

Sharing the patterns doesn’t mean sharing the overhead. On a 100-project × 3-task workspace (2026-05 numbers):

RunnerOverhead vs. raw shell, no cacheCache (full restore)
vx+2.95 s159 ms
Turbo+11.38 s589 ms
Nx+20.62 s858 ms

Full breakdown + methodology in benchmarks.md.

For features Turbo or Nx have that vx lacks, see comparison.md § Gaps.

For places vx made a deliberately different call (TypeScript config, resolved-config hash, strict output ownership, no plugins, no daemon, no TUI), see comparison.md § Where vx is ahead and README.md.

Every “Turbo” / “Nx parity” claim above is anchored in a comment in the vx source. To audit:

Terminal window
grep -rn "Turbo\|Nx parity\|turborepo" src/

That returns ~20 lines, each one a deliberate decision to mirror an upstream pattern. Adding to that list is preferable to inventing new vocabulary — vx’s value is in the swaps, not in renaming the parts we kept.