pnpm workspaces
pnpm 9 with a strict catalog and workspace protocol — single source of dependency versions across every app, package, and design-system project.
The monorepo uses pnpm 9 workspaces with a strict catalog. Every versioned dependency for every project resolves through one list.
Workspace scope
pnpm-workspace.yaml
declares which directories pnpm should treat as workspace packages:
packages:
- apps/*
- packages/*
- design-system/*
- scriptsA new app in apps/ or a new package in packages/ / design-system/ is
registered automatically by pnpm install — no manual list to update.
scripts/ is listed explicitly because it has a package.json but is not
under one of the glob roots. It exists as a workspace so tsx, @types/node,
and friends are resolvable from scripts/affected.ts, scripts/dev-server.mjs,
and other root-level tooling.
Strict catalog
The same file declares catalog: — a dictionary mapping every npm package name
to its exact version range:
catalog:
next: ^16.2.6
react: ^19.2.6
typescript: ^6.0.3
fumadocs-core: ^16.8.1
'@playwright/test': ^1.59.1
'@tanstack/react-query': ^5.99.2
# ... ~150 more
catalogMode: strictEvery project's package.json consumes versions through the catalog:
specifier:
{
"dependencies": {
"next": "catalog:",
"react": "catalog:"
}
}catalogMode: strict makes pnpm reject any direct version ("next": "^16.0.0")
in a workspace package.json. Either go through the catalog or you don't ship.
This means a React upgrade is one line in pnpm-workspace.yaml — every app
and package picks it up on the next pnpm install. There is no per-package
version drift to chase.
Workspace protocol
Internal packages reference each other through workspace:*:
{
"devDependencies": {
"@todayai-labs/tsconfig": "workspace:*",
"@todayai-labs/storybook": "workspace:*"
}
}workspace:* resolves at install time to the in-repo source. Production
builds replace it with the actual version (which doesn't matter for private
internal packages).
Adding or upgrading a dependency
# Add a new dep to the catalog manually (preferred — keeps versions visible)
# Edit pnpm-workspace.yaml under `catalog:`, then reference it in the project:
# "some-pkg": "catalog:"
# Or let pnpm pick the latest and write it to the catalog automatically
pnpm add some-pkg # in the consuming workspace
pnpm install # propagates to the restFor internal @todayai-labs/* packages, just add "@todayai-labs/<name>": "workspace:*"
to devDependencies and run pnpm install — no catalog entry needed.
onlyBuiltDependencies
pnpm 9 doesn't run lifecycle scripts of dependencies by default. The repo opts in for a small allowlist:
onlyBuiltDependencies:
- browser-tabs-lock
- esbuild
- sharp
- unrs-resolverAny other package that ships a postinstall will be silently skipped. If a
new dep needs its install script to run (rare — usually means native bindings),
add it here.
Where to look when install breaks
| Symptom | Likely culprit |
|---|---|
Catalog "default" has no entry for "X" | You added "X": "catalog:" without adding the catalog entry |
Workspace package "@todayai-labs/Y" not found | Missing @todayai-labs/Y package or the glob in pnpm-workspace.yaml doesn't cover its directory |
Cannot find module 'some-native-binding' | Add the package to onlyBuiltDependencies |
| Lockfile drift on CI | A teammate ran pnpm add without committing pnpm-lock.yaml |
Adding a package
Step-by-step checklist for adding a new app, package, or design-system project to the monorepo without breaking CI or deploys.
Moon task orchestration
Moon defines per-project build / typecheck / test tasks and the dependency graph between them. The root pnpm scripts are thin wrappers over `moon run :*`.