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.
25 KiB
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)
- Clean Architecture with Next.js (Lazar Nikolov)
- Reference repo
- Turborepo + shadcn/ui reference
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:
- Resolution table in
di/AGENTS.mdmapping every symbol to its interface, production implementation, and mock implementation. - Step-by-step registration recipe for adding new dependencies.
- tsconfig constraints documented as "do not remove" (
emitDecoratorMetadata,experimentalDecorators,reflect-metadataimport).
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.tsxand.test.tsxnext 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:
{
"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:
- Query Storybook MCP to discover existing components
- Read AGENTS.md at the target atomic level for rules
- Write component + co-located story
- Run story tests via Storybook MCP
- Autonomous fix loop if tests fail
- Visual validation via Playwright MCP
Documentation Architecture — 4 Tiers
Tier 1 — Root:
CLAUDE.md: Claude Code entry point, project overview, quick start commandsAGENTS.md: Cross-agent instructions, monorepo package map, dependency flow, hard rules, end-to-end "add a feature" recipedocs/: 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 conventionscms-core/AGENTS.md: Hook rules (do/don't), collection patterns, access controlcms-client/AGENTS.md: Dual-mode usage table, initialization patterns, standalone ruleui/AGENTS.md: Atomic classification guide, shadcn workflow, story template
Tier 3 — Layer (inside core):
entities/AGENTS.md: Zero imports rule, model template, error templateapplication/AGENTS.md: Imports entities/ only, use case template, interface naminginfrastructure/AGENTS.md: Implementation patterns, mock naming, provider namingdi/AGENTS.md: Resolution table, registration recipe, scope guidancecontrollers/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, dependenciesuse-cases/content/AGENTS.md: Content business rules, publishing workflow, error casesatoms/AGENTS.md: Classification criteria, shadcn atom list, "no margins" rulemolecules/AGENTS.md: Single responsibility rule, composition examplesorganisms/AGENTS.md: Sub-component patterns, internal state guidancetemplates/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
# 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 |