Data Nexus

Infrastructure · We run it ourselves

Turborepo

Turborepo is a task runner for JavaScript monorepos: it reads the dependency graph between packages, runs tasks in the right order, and skips work whose inputs have not changed.

The monorepo question is never about the tool. It is about whether two things must change in the same commit — a database schema and the code that reads it, an embedding format and the retrieval that depends on it — because if they must, a two-repository split turns a compile error into a production incident discovered by a customer. Turborepo is chosen when that coupling is already real, and refused when a team has one deployable and wants the folder structure of a company ten times its size.

turborepo.com

01/Why this one

  1. 01

    The test is the contract, not the file count. In mozg the shared packages are contracts/pg-schema with versioned Prisma migrations and contracts/content-db/src/embeddings.ts — a schema and an embedding format that several apps read. Change the embedding dimension or the column type in a split repository and the mismatch surfaces at runtime, in whichever service deployed second. In one repository it surfaces as a type error in the same pull request, before anything is merged. That is the entire argument for the overhead: moving a class of failure from production to compile time.


  2. 02

    A monorepo is what makes an atomic migration possible. A Prisma migration plus every call site that depends on the new column land in one commit, review as one diff, and revert as one revert. Split the repository and the same change becomes a release sequence — deploy the migration, wait, deploy the consumers — with a documented window in which the two halves disagree. Teams that split early end up writing that sequencing discipline by hand and calling it process.


  3. 03

    Turborepo's contribution is narrow and worth naming precisely: it caches task outputs keyed on inputs, so a change confined to one app does not rebuild or retest the others. Without it a monorepo has a real cost — every push runs the full matrix, and the pipeline gets slower with every package added, which is exactly how teams come to hate monorepos. The cache is what keeps the repository's growth from being paid for on every commit.


  4. 04

    The repository boundary decides where standards can be enforced once. In mozg the prettier-plugin-tailwindcss ordering, commitlint on messages, and husky with lint-staged on the pre-commit path apply to every package because there is one root to configure. Across separate repositories the same rules exist as N copies that drift, and the drift is discovered when someone reformats a file and produces a two-hundred-line diff of nothing.


  5. 05

    The refusal case is most of the work we do. This site is a single Next.js 16 application deployed to one target; pakibus is a WebGL build shipped to Cloudflare Workers with wrangler; rakairport and seahero are single Next apps. None of them shares a contract with a second deployable. Adding turbo to any of them would add a task graph, a configuration file and a cache layer to protect a build that already finishes, and would buy nothing that npm scripts do not already do. A monorepo with one app in it is a filing decision wearing a build system.


  6. 06

    Docker composition and a monorepo pull in the same direction. mozg carries compose.build.yml, compose.dev.yml and compose.override.yml alongside infra/docker and deploy.sh, which means the topology of the system — which services exist, which images they build from, how they are wired locally versus in a build — is versioned next to the code that runs inside them. Split that and the compose file describes services whose source lives elsewhere, at a commit nobody records.


  7. 07

    Two repositories are the right answer when the interface between them is a network protocol rather than a type. openclaw is a Telegram and WhatsApp gateway built on grammy and @whiskeysockets/baileys behind an express surface; it talks to what it integrates over HTTP and vendor APIs, not shared TypeScript, so folding it into a monorepo would couple release cadences for no compile-time benefit. The rule is that a monorepo is earned by a shared type, not by two projects belonging to the same company.

02/What you can check

An entry that cannot point at something you can open, run or read does not compile. That is a property of the type, not a promise in a paragraph.

In our code
mozg/ — turborepo monorepo with contracts/pg-schema/prisma/migrations (versioned Prisma migrations) and contracts/content-db/src/embeddings.ts — the shared schema and embedding contract that justify the single repository; plus compose.build.yml, compose.dev.yml, compose.override.yml, infra/docker and deploy.sh versioned with the code they run.

In our code
WITU-main/ — a second turbo monorepo, kept in that shape for the same reason: more than one deployable reading the same package.

In our code
mozg/ — repository-wide standards enforced from one root — prettier-plugin-tailwindcss, commitlint, husky and lint-staged — which is the mechanism a split repository cannot reproduce without copies that drift.

In our code
pakibus/package.json — the refusal case in practice: vite, vitest, typescript and wrangler deploying a single WebGL build to Cloudflare Workers, with no turbo and no packages directory, because there is nothing for a second package to share.

In our code
openclaw/ — kept as its own repository: grammy, @grammyjs/runner, @whiskeysockets/baileys and express behind Dockerfile plus docker-compose.yml — it integrates over network protocols, not shared types, so a monorepo would couple releases without buying compile-time safety.

Run it yourself
npx turbo run build --dry=json → the resolved task graph and, for each task, the inputs hashed and whether the cache would be hit — run this before adopting turbo: if the graph has one node, the tool has nothing to schedule and nothing to skip.

Property of the thing
Turborepo caches at task granularity keyed on declared inputs, so a task whose inputs are not fully declared — an environment variable read at build time, a file outside the package, a timestamp — is cached incorrectly and returns a stale output rather than failing loudly. — run the same task twice with the undeclared input changed between runs; a FULL TURBO cache hit on the second run, with the old output restored, confirms the input was never part of the hash.
03/Where it stops

Invariant

Turborepo does not solve dependency version conflicts, and a monorepo makes them harder to escape: every package resolves against one lockfile, so an app pinned to an older major of a shared library blocks the upgrade for all of them, and the usual workaround — pinning per package and letting the resolution tree fork — reintroduces exactly the drift the monorepo was adopted to prevent. It also has no answer for repository size. Git operations, editor indexing and CI checkout all scale with total history, and the cost is paid by every developer on every clone regardless of which app they touch, which is why the tool helps most on the axis it can hash and not at all on the one it cannot. Below roughly three deployables sharing a genuine contract, the configuration and cache-correctness burden exceeds anything returned.