Add clean architecture monorepo template design spec

Comprehensive design specification covering: Turborepo+pnpm monorepo, clean
architecture core (InversifyJS DI), tRPC+TanStack Query shared API layer,
Payload CMS with dual-mode client (local+HTTP), Atomic Design UI with
shadcn/ui+Storybook, MCP agent infrastructure, and 4-tier documentation
architecture with ~22 AGENTS.md files.
This commit is contained in:
2026-04-06 13:28:56 +02:00
commit c3c2e19e4f

View File

@@ -0,0 +1,557 @@
# 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 `<ApiProvider>` 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 |