Today Platform Web — Dev Docs
Toolchain

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/*
  - scripts

A 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: strict

Every 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 rest

For 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-resolver

Any 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

SymptomLikely culprit
Catalog "default" has no entry for "X"You added "X": "catalog:" without adding the catalog entry
Workspace package "@todayai-labs/Y" not foundMissing @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 CIA teammate ran pnpm add without committing pnpm-lock.yaml

On this page