8.2 KiB
AGENTS.md — Vertical Feature Monorepo
This is a Turborepo + pnpm monorepo organized by vertical features. Each feature package owns its own Clean Architecture layers (entities, application, infrastructure, interface-adapters) and integrations (CMS collections, tRPC routers, UI components). Core packages provide foundation: primitives, design system, CMS composition, API aggregation, and tRPC client platform.
Package Map
| Package | Tag | Purpose |
|---|---|---|
@repo/core-shared |
core | Generic primitives (Zod, env, Payload hooks/fields/blocks, tRPC init/context) |
@repo/core-cms |
core (composition) | Payload config aggregator — imports @repo/<feature>/cms only |
@repo/core-api |
core (composition) | tRPC router aggregator — imports @repo/<feature>/api only |
@repo/core-trpc |
core | Frontend tRPC client + framework-specific providers (Next.js, TanStack) |
@repo/core-ui |
core | Design system (atoms, molecules, generic organisms, templates) |
@repo/auth |
feature | Users collection + sign-in/up/out |
@repo/blog |
feature | Articles collection + article use-cases |
@repo/media |
feature | Media collection + upload helpers |
@repo/marketing-pages |
feature | Pages collection + SiteSettings global |
@repo/navigation |
feature | Header global |
@repo/eslint-config |
tooling | Shared ESLint 9 flat configs (base, next, react-internal, boundaries) |
@repo/typescript-config |
tooling | Shared TypeScript base configs + Vitest base |
Boundary Rules
Three tags
- app —
apps/web-next,apps/web-tanstack,apps/cms - feature —
packages/auth,blog,media,marketing-pages,navigation - core —
packages/core-shared,core-cms,core-api,core-trpc,core-ui - (untagged) —
packages/eslint-config,typescript-config
Allowed dependency directions
app → feature, core
feature → core
core → core (restricted; see exceptions below)
Disallowed: core → feature, core → app, feature → app, feature → feature.
Composition exceptions
Two packages may cross normal boundaries:
core-cmsmay import@repo/<feature>/cmssubpath exports only (to compose Payload collections).core-apimay import@repo/<feature>/apisubpath exports only (to compose tRPC routers).
No other cross-package boundary deviations are permitted.
Three enforcement layers
package.jsondependencies — only allowed deps are declared; illegal imports fail at install time.exportsmaps — feature packages expose.,./cms,./api,./di/bind-productiononly; no deep source paths exist.- ESLint
eslint-plugin-boundaries— configured inpackages/eslint-config/:- Feature packages may import from
core-*and tooling only. core-shared,core-trpc,core-uimay not import any feature.core-apirestricted to@repo/<feature>/apiimports.core-cmsrestricted to@repo/<feature>/cmsimports.- No
../../../cross-package relative imports.
- Feature packages may import from
Adding a Feature
Start with docs/guides/adding-a-feature.md for a step-by-step walkthrough covering:
- New feature scaffold (folder structure,
package.json,tsconfig.json,vitest.config.ts) - Clean Architecture layers (entities, use cases, repositories, DI container, controllers)
- Payload integration (collections, hooks, Payload repository binding)
- tRPC integration (routers, procedure binding)
- Core wiring (
core-api,core-cms, path aliases, app bootstrap) - Testing and lint validation
Key Commands
pnpm install # Install all dependencies
pnpm dev # Start all dev servers (Next.js :3000, CMS :3001, Storybook :6006)
pnpm typecheck # Type-check all packages
pnpm lint # Lint all packages (boundaries enforced)
pnpm test # Run all unit + integration tests (Vitest)
pnpm test:e2e # Run e2e tests (Playwright across both apps)
pnpm build # Build all packages (Turborepo)
docker compose up -d # Start PostgreSQL
# Filtered commands
pnpm dev --filter @repo/web-next # Only Next.js app
pnpm dev --filter @repo/cms # Only CMS admin
pnpm dev --filter @repo/storybook # Only Storybook
pnpm typecheck --filter @repo/blog # Only blog feature
pnpm test --filter @repo/blog # Only blog unit/integration tests
Per-Package Conventions
Source files use RELATIVE imports (not @/)
Inside src/ files, import from sibling layers using relative paths:
// packages/blog/src/application/use-cases/get-article.use-case.ts
import type { IArticlesRepository } from "../repositories/articles.repository.interface.js";
import { ARTICLES_REPOSITORY } from "../../di/symbols.js";
import type { Article } from "../../entities/article.js";
This keeps source code portable and avoids circular alias issues.
Test files use @/ alias
Test files (*.test.ts) use the @/ alias to import from src/:
// packages/blog/src/application/use-cases/get-article.use-case.test.ts
import { getArticleUseCase } from "@/application/use-cases/get-article.use-case.js";
vitest.config.ts MUST declare @/ alias
Every package's vitest.config.ts must define the alias:
import path from "path";
import { defineConfig } from "vitest/config";
export default defineConfig({
test: { environment: "node", globals: true },
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
tsconfig.json rootDir = "."
TypeScript configs must set "rootDir": "." to allow both src/ and test files to coexist:
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "dist"
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist"]
}
Payload-backed features use constructor injection
Feature packages that need Payload (e.g., @repo/blog/infrastructure/repositories/payload-articles.repository.ts) receive the Payload config via constructor, not via @repo/core-cms dependency:
export class PayloadArticlesRepository implements IArticlesRepository {
constructor(private config: Config) {}
async getById(id: string): Promise<Article | null> {
const payload = await getPayload({ config: this.config });
return payload.findByID({ collection: "articles", id });
}
}
The config comes from the app at boot time (see below).
Apps call bindProduction*() per feature at boot
Each app (web-next, web-tanstack, cms) imports feature containers and binds production Payload repos at startup:
// apps/web-next/src/app/layout.tsx (Next.js)
import { bindProductionArticles } from "@repo/blog/di/bind-production";
import { bindProductionUsers } from "@repo/auth/di/bind-production";
import { bindProductionMedia } from "@repo/media/di/bind-production";
import { payloadConfig } from "@repo/core-cms";
// At app boot:
await bindProductionArticles(blogContainer, payloadConfig);
await bindProductionUsers(authContainer, payloadConfig);
// ... etc for each feature
Specification & Guides
- Vertical Feature Spec —
docs/architecture/vertical-feature-spec.md— full design, rationale, decision log - Architecture Overview —
docs/architecture/overview.md— package responsibilities, data flow - Dependency Flow —
docs/architecture/dependency-flow.md— allowed directions and composition pattern - Adding a Feature Guide —
docs/guides/adding-a-feature.md— step-by-step new feature walkthrough - Testing Strategy —
docs/guides/testing-strategy.md— test placement, Vitest per-package, Playwright e2e
Per-package documentation lives in each AGENTS.md:
packages/core-shared/AGENTS.mdpackages/core-cms/AGENTS.mdpackages/core-api/AGENTS.mdpackages/core-trpc/AGENTS.mdpackages/core-ui/AGENTS.mdpackages/auth/AGENTS.md,blog/AGENTS.md,media/AGENTS.md,marketing-pages/AGENTS.md,navigation/AGENTS.mdpackages/eslint-config/AGENTS.md,typescript-config/AGENTS.mdapps/cms/AGENTS.md,web-next/AGENTS.md,web-tanstack/AGENTS.md,storybook/AGENTS.md