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.
Decision: Catalog or Non-Catalog?
Before adding a dependency, decide where its version should live:
| Condition | Version Location | Specifier in package.json |
|---|---|---|
| Used by 2+ workspace packages | pnpm-workspace.yaml catalog | "catalog:" |
| Must stay in lockstep with a related package already in the catalog | pnpm-workspace.yaml catalog | "catalog:" |
| Used by a single package, no cross-package constraint | That package's package.json | Direct range (e.g., "^2.1.0") |
When in doubt, start non-cataloged. Promote to the catalog when a second package needs the same dependency.
Adding a Catalog Dependency
Step 1: Add to the catalog
Add the package and version range to the catalog: section of pnpm-workspace.yaml, in the appropriate domain group:
catalog:
# Search
typesense: ^3.0.4
my-new-package: ^1.0.0 # <-- add here
Step 2: Reference from package.json
In each workspace package that needs the dependency, add it with "catalog:" as the version:
{
"dependencies": {
"my-new-package": "catalog:",
},
}
Or use the pnpm CLI, then manually replace the resolved version with "catalog:":
pnpm add my-new-package --filter @repo/api
# Then edit packages/api/package.json: change "^1.0.0" to "catalog:"
Step 3: Install
pnpm install
Step 4: Verify
pnpm install --frozen-lockfile # Must pass -- confirms lockfile consistency
pnpm ci:check # Full quality gate suite
Adding a Non-Catalog Dependency
# Add directly to the target package
pnpm add some-ui-lib --filter @repo/web
# Verify
pnpm ci:check
The version is written directly into the package's package.json. No catalog entry needed.
External SDK Boundary Check
If the dependency is an external service SDK (a client library for an external API), it must be wrapped in a dedicated @repo/* package. This is an architecture rule enforced by ESLint no-restricted-imports and dependency-cruiser.
Examples of SDK boundaries already in place:
| SDK | Wrapper Package |
|---|---|
ioredis | @repo/cache |
@anthropic-ai/sdk | @repo/ai |
@google/genai | @repo/ai |
typesense | @repo/search |
better-auth | @repo/auth |
inngest | Imported in @repo/web (background job definitions) |
If your new dependency is an external SDK:
- Create or identify the wrapper package (e.g.,
@repo/cachewrapsioredis). - Add the SDK to
SDK_BOUNDARIESinpackages/config-eslint/restrictions.js. - Run
pnpm lintto verify zero violations. - Run
pnpm dep-cruiseto verify the dependency graph.
See the Package Boundaries section in the root CLAUDE.md for the full layer hierarchy and rules.
Renovate Group Check
After adding a new dependency, check if it belongs to an existing Renovate group in renovate.json. The groups use matchPackagePatterns with regex:
| Group | Pattern | Would match... |
|---|---|---|
| drizzle | ^drizzle | drizzle-orm, drizzle-kit, drizzle-zod |
| trpc | ^@trpc/ | @trpc/client, @trpc/server |
| eslint | ^eslint, ^@eslint/, ^typescript-eslint | eslint-plugin-*, @eslint/js |
| tailwindcss | ^tailwindcss, ^@tailwindcss/ | tailwindcss, @tailwindcss/forms |
| sentry | ^@sentry/ | @sentry/nextjs, @sentry/node |
If your package matches an existing pattern, it is automatically grouped. If it does not match any pattern and is the start of a new vendor cluster, create a new group:
{
"description": "Group My Vendor packages",
"groupName": "my-vendor",
"matchPackagePatterns": ["^@my-vendor/"],
}
Complete Checklist
- Decide: catalog or non-catalog?
- If catalog: add version to
pnpm-workspace.yaml, use"catalog:"inpackage.json - If non-catalog: add directly with
pnpm add --filter - If external SDK: add to
SDK_BOUNDARIESinrestrictions.js, verify withpnpm lintandpnpm dep-cruise - Check if the package fits an existing Renovate group; create a new group if needed
- Run
pnpm install(neverpnpm update) - Run
pnpm ci:checkto validate - Commit the changes:
pnpm-workspace.yaml(if catalog),package.json,pnpm-lock.yaml, andrenovate.json(if group added)