# Clean Architecture Monorepo Template — Design Specification ## Overview A general-purpose monorepo application template based on Clean Architecture (Uncle Bob / Lazar Nikolov), designed to serve as the foundation for all future web applications. The template supports multiple frontend frameworks, integrates Payload CMS, and includes comprehensive agent-optimized documentation so AI coding agents can navigate, understand, and extend the codebase autonomously. **References:** - [Clean Architecture (Uncle Bob)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - [Clean Architecture with Next.js (Lazar Nikolov)](https://medium.com/@heinhtoo/clean-architecture-with-next-js-insights-from-lazar-nikolov-developer-advocate-at-sentry-abe1cb4c7ef3) - [Reference repo](https://github.com/heinhtoo/nextjs-clean-architecture-template) - [Turborepo + shadcn/ui reference](https://github.com/dan5py/turborepo-shadcn-ui) --- ## 1. Monorepo Infrastructure | Concern | Choice | |---|---| | Orchestrator | Turborepo | | Package manager | pnpm workspaces | | Deployment | Docker-first (docker-compose for local dev) | ### Monorepo Structure ``` template/ ├── apps/ │ ├── web-next/ # Next.js reference app │ ├── web-tanstack/ # TanStack Start reference app │ ├── cms/ # Thin Next.js shell for Payload admin │ └── storybook/ # Centralized Storybook instance │ ├── packages/ │ ├── core/ # Clean architecture core │ ├── api/ # tRPC router definitions │ ├── api-client/ # Shared React Query hooks │ ├── cms-core/ # Payload config + collections + hooks │ ├── cms-client/ # Dual-mode Payload client (local + HTTP) │ ├── ui/ # shadcn/ui + Atomic Design components │ ├── eslint-config/ # Shared linting rules │ └── typescript-config/ # Shared TS configs │ ├── tests/ │ ├── unit/ # Vitest (mirrors core structure) │ ├── integration/ # Vitest (real DB via test containers) │ └── e2e/ # Playwright (browser tests) │ ├── docs/ # Architecture guides, ADRs, diagrams ├── .mcp.json # MCP server configuration ├── docker-compose.yml # Postgres + Payload + all apps + Storybook ├── turbo.json # Turborepo task pipeline ├── pnpm-workspace.yaml # Workspace config ├── CLAUDE.md # Claude Code entry point └── AGENTS.md # Cross-agent root instructions ``` ### Framework Support Both Next.js and TanStack Start (with TanStack Router and TanStack Query) coexist as first-class reference apps. Both share the same core packages (`@repo/core`, `@repo/api-client`, `@repo/ui`), demonstrating that the clean architecture works across any frontend framework. Projects can use one or both. --- ## 2. packages/core — Clean Architecture Single `@repo/core` package organized by layer (matching Lazar's reference), with domain-based grouping inside use-cases and controllers (elements of feature-slicing). ### Layer Structure ``` packages/core/ ├── src/ │ ├── entities/ # INNERMOST: zero deps │ │ ├── models/ # Zod schemas + TS types (user, article, session, cookie) │ │ ├── errors/ # Domain errors (AuthenticationError, NotFoundError, etc.) │ │ └── AGENTS.md │ │ │ ├── application/ # USE CASES + INTERFACES │ │ ├── repositories/ # IUsersRepository, IArticlesRepository, etc. │ │ ├── services/ # IAuthService, ITelemetryService, etc. │ │ ├── use-cases/ │ │ │ ├── auth/ # sign-in, sign-up, sign-out + AGENTS.md │ │ │ └── content/ # create-article, get-articles + AGENTS.md │ │ └── AGENTS.md │ │ │ ├── infrastructure/ # IMPLEMENTATIONS │ │ ├── repositories/ # Payload, Drizzle, and mock implementations │ │ ├── services/ # Better Auth, OpenTelemetry+Sentry, mocks │ │ └── AGENTS.md │ │ │ ├── interface-adapters/ # CONTROLLERS │ │ └── controllers/ │ │ ├── auth/ # sign-in, sign-up, sign-out controllers │ │ ├── content/ # articles controller │ │ └── AGENTS.md │ │ │ └── di/ # INVERSIFYJS WIRING │ ├── container.ts # InversifyJS container │ ├── types.ts # Symbols + DI_RETURN_TYPES │ ├── modules/ # auth.module, content.module │ └── AGENTS.md # Resolution table + registration recipe │ └── AGENTS.md # Package overview + dependency rule ``` ### Dependency Rule (HARD CONSTRAINTS) | Layer | Can import from | NEVER import from | |---|---|---| | entities/ | NOTHING | Everything else | | application/ | entities/ only | infrastructure/, interface-adapters/ | | interface-adapters/ | application/, entities/ | infrastructure/ | | infrastructure/ | application/, entities/, @repo/cms-client, external libs | interface-adapters/ | | di/ | All internal layers | apps/* | ### Dependency Injection — InversifyJS The template uses InversifyJS with symbol-based resolution, following Lazar's reference implementation. Agent documentation mitigates the indirection through: 1. **Resolution table** in `di/AGENTS.md` mapping every symbol to its interface, production implementation, and mock implementation. 2. **Step-by-step registration recipe** for adding new dependencies. 3. **tsconfig constraints** documented as "do not remove" (`emitDecoratorMetadata`, `experimentalDecorators`, `reflect-metadata` import). DI modules are organized by domain (auth.module.ts, content.module.ts). Test environments swap to mock implementations via `NODE_ENV=test` checks in modules. --- ## 3. packages/api + packages/api-client — tRPC & Shared Hooks ### packages/api — tRPC Router ``` packages/api/ ├── src/ │ ├── trpc.ts # tRPC init, context, middleware │ ├── router/ │ │ ├── index.ts # Root appRouter │ │ ├── auth.router.ts # signIn, signUp, signOut procedures │ │ └── content.router.ts # articles CRUD procedures │ └── index.ts # Exports AppRouter type └── AGENTS.md ``` tRPC is the single data path for all data access, including Payload CMS content. Each tRPC procedure calls a controller from `@repo/core/interface-adapters`. Input validation uses Zod schemas from `@repo/core/entities`. Business logic never lives in routers. **tRPC HTTP handler:** Each app hosts its own tRPC endpoint. `apps/web-next` uses Next.js API routes (`app/api/trpc/[trpc]/route.ts`), `apps/web-tanstack` uses TanStack Start's server functions. Both import the `appRouter` from `@repo/api` and serve it. The router definition is shared; the HTTP transport is app-specific. ### packages/api-client — Shared React Query Hooks ``` packages/api-client/ ├── src/ │ ├── provider.tsx # tRPC + QueryClient provider │ ├── hooks/ │ │ ├── auth/ # use-sign-in, use-session │ │ ├── content/ # use-articles, use-create-article │ │ └── index.ts # Re-exports all hooks │ └── index.ts └── AGENTS.md ``` Both `apps/web-next` and `apps/web-tanstack` wrap their root with `` and use identical hooks. The hooks are framework-agnostic — they never import from Next.js or TanStack internals. --- ## 4. Payload CMS Architecture ### packages/cms-core — Payload Definition All Payload CMS configuration lives in this standalone package, not inside `apps/cms`. This includes `payload.config.ts`, all collection definitions, globals, hooks, and access control. ``` packages/cms-core/ ├── src/ │ ├── payload.config.ts # Full Payload config │ ├── collections/ │ │ ├── articles/ │ │ │ ├── index.ts # CollectionConfig │ │ │ ├── fields.ts # Field definitions │ │ │ ├── hooks/ # Thin adapters → use cases │ │ │ └── access/ # Access control rules │ │ ├── users/ │ │ └── media/ │ ├── globals/ # Site settings, navigation │ └── index.ts # Exports config + all collections └── AGENTS.md ``` `apps/cms` is a thin Next.js shell that imports the config from `@repo/cms-core` and serves the Payload admin panel. It contains almost no custom code. ### Payload Hook Architecture Hooks are categorized into two types: **CMS-operational (stay in cms-core hooks):** - Auto-generating slugs from titles - Image resizing/optimization - Populating default field values - CMS-specific access control **Business logic (delegate to use cases):** - Sending notifications on publish - Enforcing business validation rules - Updating related records across domains - Triggering workflows Business logic hooks are thin adapters (max 5-10 lines) that map Payload's hook arguments to use case inputs and call use cases from `@repo/core/application`. They never import from `@repo/core/infrastructure` or call external services directly. **Rule of thumb:** If deleting the hook would break a business requirement, the logic must be in a use case. If it would only break a CMS convenience feature, it can stay in the hook. ### packages/cms-client — Dual-Mode Payload Client ``` packages/cms-client/ ├── src/ │ ├── client.ts # createPayloadClient() │ ├── local-client.ts # Local API (direct Payload instance) │ ├── http-client.ts # HTTP REST fallback │ ├── types.ts # Generated via payload generate:types │ └── index.ts └── AGENTS.md ``` The client supports two modes: - **Local mode (primary):** Receives a Payload instance, calls `payload.find()`, `payload.findByID()`, etc. directly. Full access to Payload's query capabilities (where, sort, limit, depth, page, populate). Used by all server-side apps. - **HTTP mode (fallback):** Uses Payload's REST API. For external services that don't have access to a Payload instance. **Initialization:** The Payload instance is **injected, not imported**. At app startup, each app creates a Payload instance using the config from `@repo/cms-core` and passes it to `createPayloadClient()`. This prevents circular dependencies. The initialization code lives in each app's server entry point (e.g., `apps/web-next/src/lib/payload.ts`, `apps/web-tanstack/src/lib/payload.ts`) — it is NOT in any shared package. | Context | Mode | How | |---|---|---| | apps/cms server-side | Local | Same process as Payload | | apps/web-next server-side | Local | Initializes own Payload instance, shares DB | | apps/web-tanstack server-side | Local | Initializes own Payload instance, shares DB | | Client-side (browser) | N/A | Goes through tRPC, server handles it | | External services | HTTP | createPayloadClient({mode: "http", baseURL}) | **This package is standalone.** It never imports from `@repo/cms-core`, `@repo/core`, or `apps/*`. **Type generation:** Payload's built-in `payload generate:types` reads `payload.config.ts` from `@repo/cms-core` and outputs TypeScript types to `cms-client/src/types.ts`. This runs as a Turborepo task in the build pipeline. ### Migrations Payload CMS manages its own database migrations via `payload migrate`. This is the primary migration system since most data tables are defined as Payload collections. Drizzle migrations are optional — only needed for app-specific tables that Payload doesn't manage (e.g., session tokens, analytics, queues). --- ## 5. Data Flow Complete request lifecycle from UI to database: ``` UI Component (Next.js or TanStack Start) → useArticles() @repo/api-client hook → trpc.content.list @repo/api router procedure → articlesController.list() @repo/core/interface-adapters → getArticlesUseCase() @repo/core/application → getInjection("IArticlesRepo") InversifyJS resolves at runtime → PayloadArticlesRepository @repo/core/infrastructure → PayloadClient.find(...) @repo/cms-client (LOCAL mode) → Payload Local API Direct DB access, no HTTP ``` --- ## 6. Dependency Flow ### Package Dependencies (one direction only) ``` apps/web-next → @repo/api-client, @repo/ui Startup: @repo/cms-core (config) + @repo/cms-client (init local) apps/web-tanstack → @repo/api-client, @repo/ui Startup: @repo/cms-core (config) + @repo/cms-client (init local) apps/cms → @repo/cms-core, payload, next apps/storybook → @repo/ui @repo/api-client → @repo/api (router types only) @repo/api → @repo/core/interface-adapters (controllers) @repo/cms-core → @repo/core/application (use cases for hooks), payload (types) @repo/cms-client → (standalone — receives Payload instance, doesn't import it) @repo/ui → (standalone — tailwind, shadcn) ``` ### Circular Dependency Prevention — HARD RULES These rules are non-negotiable and enforced via documentation + linting: - **NEVER:** packages/core → apps/* - **NEVER:** apps/cms → packages/core/infrastructure - **NEVER:** packages/cms-client → apps/cms or packages/core or packages/cms-core - **NEVER:** packages/cms-core → packages/cms-client - **NEVER:** core/entities → anything - **NEVER:** core/application → core/infrastructure --- ## 7. Technology Stack | Concern | Choice | Architecture Layer | |---|---|---| | Monorepo | Turborepo + pnpm workspaces | Infrastructure | | Frameworks | Next.js + TanStack Start (coexist) | Frameworks & Drivers | | CMS | Payload CMS 3.x (standalone in cms-core) | Frameworks & Drivers | | CMS Client | Dual-mode: Local API (primary) + HTTP (fallback) | Infrastructure | | API | tRPC (single data path, wraps all data) | Interface Adapters | | DI | InversifyJS + agent documentation | Frameworks & Drivers | | Validation | Zod | All layers | | Database | Agnostic → Drizzle + PostgreSQL (optional, alongside Payload) | Infrastructure | | Auth | Agnostic → Better Auth default | Infrastructure | | Observability | OpenTelemetry interfaces → Sentry backend | Infrastructure | | State (server) | TanStack Query (via tRPC) | Frameworks & Drivers | | State (client) | Zustand | Frameworks & Drivers | | Styling | Tailwind CSS v4 + shadcn/ui (@repo/ui) | Frameworks & Drivers | | UI Architecture | Atomic Design (atoms, molecules, organisms, templates) | Frameworks & Drivers | | Testing (unit/integ) | Vitest | All layers | | Testing (E2E) | Playwright | Frameworks & Drivers | | Deployment | Docker-first + docker-compose | Infrastructure | | Migrations | Payload primary, Drizzle optional | Infrastructure | | Type generation | payload generate:types → cms-client/types.ts | Build pipeline | --- ## 8. UI Architecture — Atomic Design + shadcn/ui + Storybook ### @repo/ui Package Structure ``` packages/ui/ ├── src/ │ ├── atoms/ # shadcn primitives + custom atoms │ │ ├── button/ │ │ │ ├── button.tsx # Component │ │ │ ├── button.stories.tsx # Co-located Storybook story │ │ │ ├── button.test.tsx # Unit test │ │ │ └── index.ts # Export │ │ ├── input/ │ │ ├── label/ │ │ ├── badge/ │ │ ├── ... (separator, skeleton, avatar, icon, spinner, etc.) │ │ ├── index.ts # Re-exports all atoms │ │ └── AGENTS.md │ │ │ ├── molecules/ # 2-3 atoms combined, single responsibility │ │ ├── form-field/ # Label + Input + Error │ │ ├── search-bar/ # Input + Button + Icon │ │ ├── tooltip/ │ │ ├── popover/ │ │ ├── select/ │ │ ├── index.ts │ │ └── AGENTS.md │ │ │ ├── organisms/ # Complex, self-contained UI sections │ │ ├── data-table/ # With sub-components (header, pagination) │ │ ├── dialog/ │ │ ├── card/ │ │ ├── header/ │ │ ├── sidebar/ │ │ ├── command-palette/ │ │ ├── index.ts │ │ └── AGENTS.md │ │ │ ├── templates/ # Page-level layouts with content slots │ │ ├── dashboard-layout/ │ │ ├── auth-layout/ │ │ ├── content-layout/ │ │ ├── index.ts │ │ └── AGENTS.md │ │ │ ├── hooks/ # Shared UI hooks (use-media-query, use-debounce) │ ├── lib/ # Utilities (cn() helper) │ └── styles/ # globals.css, design tokens │ ├── components.json # shadcn/ui config (aliases point to atoms/) ├── tailwind.config.ts └── AGENTS.md # Package overview + atomic classification guide ``` ### Atomic Design Import Rules | Level | Can import from | NEVER import from | |---|---|---| | Atoms | lib/, hooks/, styles/ | molecules/, organisms/, templates/ | | Molecules | atoms/, lib/, hooks/ | organisms/, templates/ | | Organisms | atoms/, molecules/, lib/, hooks/ | templates/ | | Templates | atoms/, molecules/, organisms/, lib/, hooks/ | (top level) | | Pages | Everything from @repo/ui + @repo/api-client | **Live in apps/, NOT in @repo/ui** | ### Component Rules - **Atoms:** No margins/positioning, no state, no business logic. Pure visual elements. - **Molecules:** Single responsibility, minimal controlled state. Combine 2-3 atoms. - **Organisms:** Can have internal state and sub-components. Self-contained sections. - **Templates:** Use children/slots for content. NEVER hard-code content. - **All levels:** Co-locate `.stories.tsx` and `.test.tsx` next to the component. ### shadcn/ui Integration `pnpm ui add [component]` lands components in `atoms/` by default (configured via `components.json` aliases). After adding, the component is classified using the guide in `AGENTS.md` and relocated to the correct atomic level if needed. ### Storybook `apps/storybook` is a centralized Storybook instance using `@storybook/react-vite`. It pulls stories from `packages/ui/src/**/*.stories.tsx`. Story titles follow the pattern `"Level/ComponentName"` (e.g., `"Atoms/Button"`, `"Organisms/DataTable"`), creating a sidebar organized by atomic level. --- ## 9. Agent Infrastructure ### MCP Server Configuration Project-level `.mcp.json` in the monorepo root, shared via git: ```json { "mcpServers": { "storybook": { "type": "http", "url": "http://localhost:6006/mcp" }, "playwright": { "type": "stdio", "command": "npx", "args": ["@anthropic-ai/playwright-mcp"] } } } ``` **Storybook MCP** (via `@storybook/addon-mcp` in `apps/storybook`): - Component discovery: `list-all-documentation` - Component docs: `get-documentation`, `get-documentation-for-story` - Story authoring: `get-storybook-story-instructions`, `preview-stories` - Testing: `run-story-tests` (accessibility + interaction tests with autonomous fix loop) **Playwright MCP:** - Browser automation for E2E validation - Accessibility snapshots - Visual verification of rendered components ### Agent Workflow When building UI: 1. Query Storybook MCP to discover existing components 2. Read AGENTS.md at the target atomic level for rules 3. Write component + co-located story 4. Run story tests via Storybook MCP 5. Autonomous fix loop if tests fail 6. Visual validation via Playwright MCP ### Documentation Architecture — 4 Tiers **Tier 1 — Root:** - `CLAUDE.md`: Claude Code entry point, project overview, quick start commands - `AGENTS.md`: Cross-agent instructions, monorepo package map, dependency flow, hard rules, end-to-end "add a feature" recipe - `docs/`: Architecture guides, how-to guides, ADRs, Mermaid diagrams **Tier 2 — Package:** Each package gets an `AGENTS.md` with: purpose, public API, import rules, step-by-step recipes for common tasks. Key packages have specialized content: - `core/AGENTS.md`: Layer diagram, import rules table, DI resolution table, naming conventions - `cms-core/AGENTS.md`: Hook rules (do/don't), collection patterns, access control - `cms-client/AGENTS.md`: Dual-mode usage table, initialization patterns, standalone rule - `ui/AGENTS.md`: Atomic classification guide, shadcn workflow, story template **Tier 3 — Layer (inside core):** - `entities/AGENTS.md`: Zero imports rule, model template, error template - `application/AGENTS.md`: Imports entities/ only, use case template, interface naming - `infrastructure/AGENTS.md`: Implementation patterns, mock naming, provider naming - `di/AGENTS.md`: Resolution table, registration recipe, scope guidance - `controllers/AGENTS.md`: Validate → call use case pattern, error mapping **Tier 4 — Domain (business logic):** - `use-cases/auth/AGENTS.md`: Auth business rules, invariants, error cases, dependencies - `use-cases/content/AGENTS.md`: Content business rules, publishing workflow, error cases - `atoms/AGENTS.md`: Classification criteria, shadcn atom list, "no margins" rule - `molecules/AGENTS.md`: Single responsibility rule, composition examples - `organisms/AGENTS.md`: Sub-component patterns, internal state guidance - `templates/AGENTS.md`: Content slots pattern, "never hard-code content" rule **Total: ~22 AGENTS.md files, ~16 docs files.** ### docs/ Folder Structure ``` docs/ ├── architecture/ │ ├── overview.md # High-level architecture diagram │ ├── clean-architecture.md # Uncle Bob's principles applied │ ├── dependency-flow.md # Complete dependency graph │ ├── data-flow.md # Request lifecycle │ └── circular-dep-prevention.md # Rules + examples │ ├── guides/ │ ├── adding-a-feature.md # End-to-end walkthrough │ ├── adding-a-collection.md # Payload CMS collection │ ├── adding-a-component.md # Atomic design classification │ ├── testing-strategy.md # What to test at each layer │ ├── deployment.md # Docker build + deploy │ └── mcp-setup.md # Storybook MCP + Playwright MCP │ ├── decisions/ │ ├── adr-001-monorepo-tool.md # Why Turborepo + pnpm │ ├── adr-002-di-framework.md # Why InversifyJS │ ├── adr-003-cms-separation.md # Why cms-core vs cms-client │ ├── adr-004-dual-mode-client.md # Why local + HTTP modes │ └── adr-005-atomic-design.md # Why atomic design for UI │ └── diagrams/ ├── monorepo-structure.md # Mermaid diagram ├── dependency-graph.md # Mermaid diagram └── data-flow.md # Mermaid diagram ``` --- ## 10. Docker & Local Development ```yaml # docker-compose.yml services: postgres: image: postgres:16-alpine ports: ["5432:5432"] cms: build: ./apps/cms depends_on: [postgres] ports: ["3001:3000"] # Payload admin at localhost:3001 web-next: build: ./apps/web-next depends_on: [cms] ports: ["3000:3000"] # Next.js at localhost:3000 web-tanstack: build: ./apps/web-tanstack depends_on: [cms] ports: ["3002:3000"] # TanStack at localhost:3002 storybook: build: ./apps/storybook ports: ["6006:6006"] # Storybook at localhost:6006 ``` One command: `docker compose up` — spins up Postgres, Payload CMS admin, both reference apps, and Storybook. --- ## 11. Testing Strategy | Layer | Tool | What to test | |---|---|---| | Entities | Vitest (unit) | Zod schema validation, error classes | | Use cases | Vitest (unit) | Business logic with mock implementations via DI | | Controllers | Vitest (unit) | Input validation, use case delegation, error mapping | | Infrastructure | Vitest (integration) | Real DB via test containers, Payload API calls | | UI components | Vitest (unit) + Storybook | Rendering, props, accessibility | | Full app | Playwright (E2E) | User flows across both Next.js and TanStack Start |