Files
agentic-dev/docs/superpowers/specs/2026-04-06-clean-architecture-monorepo-template-design.md
Danijel Martinek c3c2e19e4f 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.
2026-04-06 13:28:56 +02:00

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:


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:

{
  "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

# 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