diff --git a/docs/architecture/dependency-flow.md b/docs/architecture/dependency-flow.md new file mode 100644 index 0000000..3343269 --- /dev/null +++ b/docs/architecture/dependency-flow.md @@ -0,0 +1,50 @@ +# 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) +``` + +## Core Internal Dependencies + +``` +core/entities → (standalone — zero deps) +core/application → core/entities only +core/interface-adapters → core/application, core/entities +core/infrastructure → core/application, core/entities, @repo/cms-client, external libs +core/di → all internal layers +``` + +## Circular Dependency Prevention — HARD RULES + +- **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 + +## Why These Rules Exist + +The Payload CMS integration creates a potential circular dependency: +``` +apps/cms hooks → @repo/core/application (use cases) +@repo/core/infrastructure → @repo/cms-client → Payload API +``` + +This is resolved by: +1. `cms-client` is standalone — receives Payload instance via injection, never imports it +2. `cms-core` hooks only import from `core/application`, never `core/infrastructure` +3. App startup code (not a shared package) wires Payload instance into cms-client diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..be8646a --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,55 @@ +# Architecture Overview + +## Clean Architecture Monorepo + +This template implements Uncle Bob's Clean Architecture in a Turborepo + pnpm monorepo. The core principle: **dependencies point inward only**. + +``` +┌─────────────────────────────────────────────────┐ +│ Frameworks & Drivers (outermost) │ +│ Next.js, TanStack Start, Payload CMS, │ +│ Storybook, PostgreSQL, Docker │ +│ ┌─────────────────────────────────────────┐ │ +│ │ Interface Adapters │ │ +│ │ tRPC routers, Controllers, Presenters │ │ +│ │ ┌─────────────────────────────────┐ │ │ +│ │ │ Application (Use Cases) │ │ │ +│ │ │ Business logic, interfaces │ │ │ +│ │ │ ┌─────────────────────────┐ │ │ │ +│ │ │ │ Entities (innermost) │ │ │ │ +│ │ │ │ Models, Errors, Zod │ │ │ │ +│ │ │ └─────────────────────────┘ │ │ │ +│ │ └─────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +## Package Map + +``` +packages/core → Clean architecture (entities, use cases, infra, DI) +packages/api → tRPC routers (calls core controllers) +packages/api-client → Shared React Query hooks + provider +packages/cms-core → Payload CMS config, collections, hooks +packages/cms-client → Dual-mode Payload client (local + HTTP) +packages/ui → Atomic Design + shadcn/ui + Tailwind v4 + +apps/web-next → Next.js 15 reference app +apps/web-tanstack → TanStack Start reference app +apps/cms → Thin Next.js shell for Payload admin +apps/storybook → Centralized Storybook instance +``` + +## Data Flow + +``` +UI → useQuery(trpc.content.list) → tRPC Router → Controller → Use Case → Repository → Payload Local API → PostgreSQL +``` + +## Key Design Decisions + +- **InversifyJS DI** — symbol-based resolution, documented in di/AGENTS.md +- **Dual-mode CMS client** — Local API for server-side (no HTTP overhead), HTTP for external +- **Atomic Design** — atoms/molecules/organisms/templates, co-located stories +- **tRPC single data path** — all data (including CMS content) flows through tRPC +- **Agent-optimized docs** — AGENTS.md at every level with rules, recipes, tables diff --git a/docs/decisions/adr-001-monorepo-tool.md b/docs/decisions/adr-001-monorepo-tool.md new file mode 100644 index 0000000..a1d870f --- /dev/null +++ b/docs/decisions/adr-001-monorepo-tool.md @@ -0,0 +1,19 @@ +# ADR-001: Turborepo + pnpm for Monorepo + +## Status: Accepted + +## Context + +Need a monorepo tool for a clean architecture template with multiple apps and shared packages. + +## Decision + +Turborepo + pnpm workspaces. + +## Rationale + +- Clean architecture already provides organizational structure — Nx's opinions would compete +- Minimal config (turbo.json + pnpm-workspace.yaml) means agents understand the setup quickly +- Excellent caching — shared packages build once and are reused +- Battle-tested pairing, both reference repos use it +- Works naturally with Vite for non-Next packages diff --git a/docs/decisions/adr-002-di-framework.md b/docs/decisions/adr-002-di-framework.md new file mode 100644 index 0000000..2fc8ddf --- /dev/null +++ b/docs/decisions/adr-002-di-framework.md @@ -0,0 +1,26 @@ +# ADR-002: InversifyJS for Dependency Injection + +## Status: Accepted + +## Context + +Need DI for clean architecture. Options: InversifyJS, tsyringe, manual composition root. + +## Decision + +InversifyJS with symbol-based resolution + targeted agent documentation. + +## Rationale + +- Scales to 20+ services with automatic dependency chain resolution +- Built-in singleton/transient/request scopes +- Middleware support for logging/tracing (Sentry integration) +- Matches the reference implementation by Lazar Nikolov +- Agent readability gap (4/10 → 7/10) mitigated by resolution tables and step-by-step recipes in di/AGENTS.md +- Familiar to developers from Java/C# backgrounds + +## Trade-offs + +- Requires reflect-metadata + decorator config in tsconfig +- Symbol indirection harder to trace than plain functions +- Extra dependency (inversify + reflect-metadata) diff --git a/docs/decisions/adr-003-cms-separation.md b/docs/decisions/adr-003-cms-separation.md new file mode 100644 index 0000000..c2e480f --- /dev/null +++ b/docs/decisions/adr-003-cms-separation.md @@ -0,0 +1,27 @@ +# ADR-003: CMS Core + CMS Client Separation + +## Status: Accepted + +## Context + +Payload CMS needs to integrate with clean architecture without creating circular dependencies. + +## Decision + +Three packages: `@repo/cms-core` (config + collections), `@repo/cms-client` (standalone typed client), `apps/cms` (thin shell). + +## Rationale + +- `cms-core` is testable independently — collections are just config objects +- `cms-client` is standalone (no internal imports) — prevents circular deps +- `apps/cms` is almost empty — just boots Next.js with cms-core's config +- Payload instance injected into cms-client, not imported — app startup code wires them + +## Circular Dependency Prevention + +``` +apps/cms → @repo/cms-core → @repo/core/application (hooks) +@repo/core/infrastructure → @repo/cms-client (standalone) +``` + +No cycles because cms-client never imports from cms-core or core. diff --git a/docs/decisions/adr-004-dual-mode-client.md b/docs/decisions/adr-004-dual-mode-client.md new file mode 100644 index 0000000..459b6af --- /dev/null +++ b/docs/decisions/adr-004-dual-mode-client.md @@ -0,0 +1,18 @@ +# ADR-004: Dual-Mode Payload Client (Local + HTTP) + +## Status: Accepted + +## Context + +Apps need to access Payload CMS data. Payload 3.x offers both Local API (direct) and REST API (HTTP). + +## Decision + +`@repo/cms-client` supports both modes via `createPayloadClient()`. + +## Rationale + +- **Local mode (primary):** Direct Payload instance access — no HTTP overhead, full query capabilities (where, sort, limit, depth, page, populate). All server-side apps use this. +- **HTTP mode (fallback):** REST API for external consumers without access to a Payload process. +- Payload instance is injected at app startup, not imported — keeps cms-client standalone. +- Both modes share the same `PayloadClient` interface — consumers don't know which mode is active. diff --git a/docs/decisions/adr-005-atomic-design.md b/docs/decisions/adr-005-atomic-design.md new file mode 100644 index 0000000..8960e46 --- /dev/null +++ b/docs/decisions/adr-005-atomic-design.md @@ -0,0 +1,20 @@ +# ADR-005: Atomic Design for UI Components + +## Status: Accepted + +## Context + +Need a scalable component architecture for the shared UI package. + +## Decision + +Atomic Design (atoms/molecules/organisms/templates) + shadcn/ui + Storybook. + +## Rationale + +- Clear hierarchy makes agents know exactly where to place new components +- Import rules enforce composition direction (atoms never import molecules) +- Co-located stories make components self-documenting +- shadcn/ui provides excellent base atoms that map naturally to atomic levels +- Storybook sidebar mirrors the hierarchy via story titles +- Pages live in apps (not UI package) — they connect to real data diff --git a/docs/guides/adding-a-feature.md b/docs/guides/adding-a-feature.md new file mode 100644 index 0000000..a825e4e --- /dev/null +++ b/docs/guides/adding-a-feature.md @@ -0,0 +1,70 @@ +# Adding a New Feature — End-to-End Guide + +This guide walks through adding a complete feature from entity to UI. + +## Example: Adding a "Comments" feature + +### 1. Define Entity + +Create `packages/core/src/entities/models/comment.ts`: +```typescript +import { z } from "zod"; +export const commentSchema = z.object({ + id: z.string(), + content: z.string().min(1), + articleId: z.string(), + authorId: z.string(), + createdAt: z.date(), +}); +export type Comment = z.infer; +``` +Export from `entities/models/index.ts`. + +### 2. Define Repository Interface + +Create `packages/core/src/application/repositories/comments.repository.interface.ts`: +```typescript +import type { Comment } from "@/entities/models/comment.js"; +export interface ICommentsRepository { + getCommentsByArticle(articleId: string): Promise; + createComment(input: Comment): Promise; +} +``` +Export from `application/repositories/index.ts`. + +### 3. Write Use Case (TDD) + +Write test first in `packages/core/tests/unit/use-cases/content/create-comment.use-case.test.ts`, then implement in `packages/core/src/application/use-cases/content/create-comment.use-case.ts`. + +### 4. Write Controller + +Create `packages/core/src/interface-adapters/controllers/content/comments.controller.ts` — validates input with Zod, calls use case. + +### 5. Write Mock Implementation + +Create `packages/core/src/infrastructure/repositories/mock-comments.repository.ts` with `@injectable()`. + +### 6. Register in DI + +Add `ICommentsRepository` symbol to `di/types.ts`, create binding in `di/modules/content.module.ts`, load in `di/container.ts`. + +### 7. Add tRPC Router + +Create `packages/api/src/router/comments.router.ts`, add to `appRouter` in `router/index.ts`. + +### 8. (Optional) Add Payload Collection + +If comments are stored in Payload CMS, create `packages/cms-core/src/collections/comments/`. + +### 9. Build UI + +Create component in `packages/ui/src/organisms/comment-list/` with co-located `.stories.tsx`. + +### 10. Wire in App + +Use `useTRPC()` in app pages to fetch and display comments. + +### 11. Write Tests + +- Unit: use case + controller tests in `packages/core/tests/` +- E2E: Playwright test in `tests/e2e/` diff --git a/docs/guides/testing-strategy.md b/docs/guides/testing-strategy.md new file mode 100644 index 0000000..ac6a85c --- /dev/null +++ b/docs/guides/testing-strategy.md @@ -0,0 +1,41 @@ +# Testing Strategy + +## Test Layers + +| 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 + Storybook | Rendering, props, accessibility | +| Full App | Playwright (E2E) | User flows across both Next.js and TanStack Start | + +## Running Tests + +```bash +pnpm test # All tests via Turborepo +cd packages/core && pnpm vitest run # Core unit tests only +cd packages/core && pnpm vitest watch # Core tests in watch mode +``` + +## Test Pattern (DI Container) + +All tests that use the DI container must initialize and destroy it: + +```typescript +import "reflect-metadata"; +import { beforeEach, afterEach } from "vitest"; +import { initializeContainer, destroyContainer } from "@/di/container.js"; + +beforeEach(() => { initializeContainer(); }); +afterEach(() => { destroyContainer(); }); +``` + +This ensures each test gets fresh mock instances (singleton scope resets). + +## Test File Location + +- Core tests: `packages/core/tests/unit/{use-cases,controllers}/{domain}/` +- UI tests: co-located next to component (`*.test.tsx`) +- E2E tests: `tests/e2e/`