package.json ownership
rule
each package declares only the dependencies, lifecycle tasks, and lint/format configuration it owns. the root coordinates the Turbo workspace; it is not a convenience-script dumping ground.
why
every field in a workspace has one runtime owner, and only that owner can answer its questions: is this update breaking for me, does removing this package break me, which lifecycle names do i own. when ownership blurs into the root, those questions become coordination problems, update tools act on dependencies they do not own, and the graph turns opaque. invariant: each package is independently installable, buildable, and verifiable at its own boundary.
decide ownership before adding a field
package.json is the smallest executable boundary. before adding a dependency, script, export, or override, name the package that owns the runtime behavior — the field belongs there. the root may coordinate the graph but must not impersonate an application or library.
what blurred ownership costs:
- root holds a frontend dependency → backend engineers install a build-time Next.js they never use.
- frontend version updated at root → nobody knows if it is a frontend breaking change or a root choice; a deployer cannot decide to deploy or wait.
- two packages need the same library at different versions → the root owns neither consumer, both versions stay incompatible in the lockfile.
- Renovate/Dependabot scans the root → finds dependencies it does not own, giving premature or missed updates.
| change | owner | reason |
|---|---|---|
| a browser-only runtime library | consuming application | part of that application's deployed runtime. |
| a generated client used by several applications | generated contract package | one generated surface prevents consumers from inventing different clients. |
| formatter or ESLint configuration | owning package or its local config/ folder | formatting/linting must be independently runnable at the package boundary. |
| a deploy command | deployable application | deployment needs that application's environment and output contract. |
required shape
private: trueunless publication is an explicit product decision.type: modulefor authored TypeScript ESM packages.- expose applicable canonical scripts:
dev,generate,build,format,lint,typecheck, anddeployonly where the package owns deployment. - every package implements its relevant lifecycle names itself; never delegate
formatorlintto a root script. - keep runtime dependencies separate from development tooling.
root contract
the root package.json is a thin Turbo control plane. it may declare: the package manager, workspace metadata, Turbo itself, and the single pipeline entrypoint orchestrating package tasks. it must not acquire package dependencies, shared ESLint/formatter configurations, or aliases such as format:all, lint:all, and package-specific build scripts.
canonical pipeline — resolved install, then the same package lifecycle names through Turbo:
pnpm install && turbo run generate format build lint typecheck
script contract
each script name is a stable lifecycle phase, not a convenience alias:
generatecreates declared derived artifactsbuildproduces deployable or consumable outputtypecheckchecks the authored and generated type surfacedevstarts a persistent local process
if a package cannot own the command's inputs, outputs, and failure boundary, it should not expose that script.
do not
- put a dependency in the root because one app needs it.
- add root aliases that obscure which package owns an operation.
- place shared ESLint or formatter behavior in the root as a default for packages that lack their own configuration.
- hand-edit generated package metadata.
good and bad
bad
{"scripts": { "deploy:frontend": "pnpm --filter frontend deploy" },"dependencies": { "next": "latest" }}
the root owns a frontend runtime dependency and hides the deploy boundary behind an alias: every package install pulls a runtime it never uses, update tools cannot attribute dependencies to a consumer, and no one can tell a frontend breaking change from a root choice. ownership questions become coordination problems — the slowest, most fragile way to resolve them.
good
{"scripts": { "build": "turbo run build" },"devDependencies": { "turbo": "^2.5.8" }}
the root coordinates packages; the frontend package owns its own Next dependency and deploy script. the graph answers questions locally: a dependency update is evaluated by its only consumer, a deploy runs from the package that owns its environment, and the same lifecycle name means the same thing in every owner. each package stays independently installable, buildable, and verifiable — adding or extracting one does not reopen decisions about the others.
verification
- dependency change: inspect the lockfile edge; would removing the package break this package at runtime or only its tooling?
- script change: run it through the owning package and through the root graph; the same lifecycle name must not mean conflicting things in two owners.