Version Strategy
Version range conventions, pnpm overrides, intentional pins, and the monthly deep-check process.
Range Convention
All catalog entries and most direct dependencies use caret ranges (^):
^4.3.6 = >=4.3.6 <5.0.0
Caret ranges allow Renovate to propose patch and minor updates without requiring a catalog change. When a new minor version is published, Renovate updates the lockfile. When a new major version is published, it opens a PR that bumps the catalog entry itself (which requires manual review).
The exception is packages pinned to an exact version. Currently there are none in the catalog, but the convention is to pin only when:
- A specific version has a known regression in a newer release
- The package has unstable minor releases (rare in the ecosystem today)
- An integration test has proven incompatibility with a specific version range
Intentional Version Constraints
TypeScript: Pinned to 5.x
# pnpm-workspace.yaml
typescript: ^5.9.3
TypeScript is constrained to the 5.x line because typescript-eslint does not yet support TypeScript 6.x. The comment in pnpm-workspace.yaml documents this:
# TypeScript (pinned to 5.x -- typescript-eslint doesn't support 6.x yet)
typescript: ^5.9.3
When typescript-eslint adds 6.x support, remove the constraint and let Renovate propose the major bump.
Lockstep Groups
Some packages must update together. The catalog enforces this by listing them at the same version:
| Group | Packages | Shared Version |
|---|---|---|
| tRPC | @trpc/client, @trpc/react-query, @trpc/server | ^11.15.0 |
| React | react, react-dom | ^19.2.4 |
| Tailwind | tailwindcss, @tailwindcss/postcss | ^4.2.2 |
| Vitest | vitest, @vitest/coverage-v8 | ^4.1.2 |
| Fumadocs | fumadocs-core, fumadocs-ui | ^16.7.10 |
The catalog makes this explicit. If someone bumps @trpc/client but forgets @trpc/server, the version mismatch is visible in a single file (pnpm-workspace.yaml), not scattered across five package.json files.
Renovate reinforces this through package groups that update all related packages in one PR.
pnpm Overrides
The root package.json defines pnpm.overrides for transitive dependency fixes:
{
"pnpm": {
"overrides": {
"vite": "^8.0.5",
"defu": ">=6.1.5",
"minimatch@<4>brace-expansion": "1.1.13",
},
},
}
When to use overrides
Overrides force a specific version of a transitive dependency (one you do not directly import but that appears in your dependency tree). Use them when:
- Security vulnerability -- a transitive dependency has a CVE and the direct dependency has not released a fix yet.
- Compatibility fix -- a transitive dependency version causes build or runtime errors. The
viteoverride exists because a newer Vite is needed for compatibility with the current build toolchain. - Sub-dependency pin -- the
minimatch@<4>brace-expansionoverride pins a specific sub-dependency of an older minimatch version to avoid a known issue.
Override hygiene
Overrides are technical debt. Each one should have a comment explaining why it exists and under what condition it can be removed. Review overrides during the monthly deep-check and remove any that are no longer needed (i.e., the upstream package has released a fix).
onlyBuiltDependencies
The root package.json also lists packages in pnpm.onlyBuiltDependencies:
{
"pnpm": {
"onlyBuiltDependencies": [
"@sentry/cli",
"@vvago/vale",
"esbuild",
"protobufjs",
"sharp",
"unrs-resolver",
],
},
}
This restricts which packages are allowed to run install scripts (postinstall, preinstall). Packages not in this list have their install scripts skipped, which improves install speed and reduces supply chain risk. When adding a dependency that requires a build step during install (native binaries, WASM compilation), add it to this list.
Monthly Deep-Check
Renovate handles the steady stream of individual package updates. The monthly deep-check catches cross-dependency interaction issues that individual PRs miss.
Process
# Install ncu globally (one-time setup)
pnpm add -g npm-check-updates
# Run the doctor check -- updates one package at a time, tests after each
ncu --doctor -u
# If updates were applied, verify the full suite
pnpm install
pnpm ci:full # ci:check + build
# Commit if everything passes
git add package.json pnpm-lock.yaml **/package.json
git commit -m "chore: monthly dependency update via ncu --doctor"
What ncu --doctor does
- Updates a single dependency to its latest version.
- Runs the test suite (
pnpm test). - If tests pass, keeps the update and moves to the next dependency.
- If tests fail, reverts the update and reports it as incompatible.
- At the end, you have a clean set of safe updates applied.
When to run
Pick a consistent day each month (e.g., first Saturday). This complements Renovate -- Renovate handles the steady stream, ncu --doctor catches anything that fell through the cracks or that only breaks when combined with other updates.
Review overrides during deep-check
While running the monthly check, also review each pnpm.overrides entry:
- Check if the upstream dependency has released a version that fixes the issue the override addresses.
- If fixed, remove the override, run
pnpm install, and verify withpnpm ci:full. - If still needed, leave a comment with the current status and expected resolution timeline.
Related
- Catalog Pattern -- the version source of truth
- Renovate Automation -- how automated updates flow in
- Pipeline -- the CI that validates every dependency change
Adding Dependencies
Step-by-step guide for adding a new dependency to the monorepo -- catalog vs. non-catalog, SDK boundary rules, and Renovate group configuration.
Architecture Enforcement
Automated multi-layer system that catches architectural violations at editor-time, pre-commit, and CI -- designed for a codebase built primarily by AI coding agents.