feat: add docs/ folder (architecture, guides, ADRs)

This commit is contained in:
2026-04-06 15:05:35 +02:00
parent 2c110b33e9
commit a22c9bdaed
9 changed files with 326 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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<typeof commentSchema>;
```
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<Comment[]>;
createComment(input: Comment): Promise<Comment>;
}
```
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/`

View File

@@ -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/`