From c3c2e19e4fcfaf09161ddef0e78f10ec85432b2f Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Mon, 6 Apr 2026 13:28:56 +0200 Subject: [PATCH] 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. --- ...n-architecture-monorepo-template-design.md | 557 ++++++++++++++++++ 1 file changed, 557 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-06-clean-architecture-monorepo-template-design.md diff --git a/docs/superpowers/specs/2026-04-06-clean-architecture-monorepo-template-design.md b/docs/superpowers/specs/2026-04-06-clean-architecture-monorepo-template-design.md new file mode 100644 index 0000000..2d5fecc --- /dev/null +++ b/docs/superpowers/specs/2026-04-06-clean-architecture-monorepo-template-design.md @@ -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 `` 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 |