Infrastructure · We run it ourselves
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.