docs(plans): delete six stale 2026-04-06 plan docs (superseded by 2026-05-04-plan-{1..6})

This commit is contained in:
2026-05-05 09:34:44 +02:00
parent 8dac1929dc
commit 588f47affa
7 changed files with 0 additions and 5385 deletions

View File

@@ -1,965 +0,0 @@
# Plan 1: Monorepo Foundation — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Scaffold a Turborepo + pnpm monorepo with shared TypeScript and ESLint configs, placeholder packages for all planned workspaces, and Docker Compose for local development.
**Architecture:** Turborepo orchestrates builds across pnpm workspaces. Shared config packages (`@repo/typescript-config`, `@repo/eslint-config`) provide consistent tooling. All apps and packages are created as empty placeholders with correct `package.json` files so the workspace graph is valid from the start. Docker Compose provides PostgreSQL for local development.
**Tech Stack:** Turborepo 2.x, pnpm 9.x, TypeScript 5.x, ESLint 9.x (flat config), Vitest, Docker Compose, PostgreSQL 16
---
## File Map
| File | Responsibility |
|---|---|
| `package.json` | Root workspace manifest, delegates to turbo |
| `pnpm-workspace.yaml` | Declares workspace packages |
| `turbo.json` | Task pipeline (build, dev, lint, test, typecheck) |
| `.npmrc` | pnpm workspace settings |
| `.gitignore` | Ignore patterns for Turborepo + pnpm + Node.js |
| `packages/typescript-config/package.json` | Shared TS config package manifest |
| `packages/typescript-config/base.json` | Base TypeScript config |
| `packages/typescript-config/nextjs.json` | Next.js TypeScript config |
| `packages/typescript-config/react-library.json` | React library TypeScript config |
| `packages/eslint-config/package.json` | Shared ESLint config package manifest |
| `packages/eslint-config/base.js` | Base ESLint flat config |
| `packages/eslint-config/next.js` | Next.js ESLint config |
| `packages/eslint-config/react-internal.js` | React library ESLint config |
| `packages/core/package.json` | Placeholder — clean architecture core |
| `packages/core/tsconfig.json` | Extends @repo/typescript-config/base |
| `packages/api/package.json` | Placeholder — tRPC routers |
| `packages/api/tsconfig.json` | Extends @repo/typescript-config/base |
| `packages/api-client/package.json` | Placeholder — React Query hooks |
| `packages/api-client/tsconfig.json` | Extends @repo/typescript-config/react-library |
| `packages/cms-core/package.json` | Placeholder — Payload CMS definition |
| `packages/cms-core/tsconfig.json` | Extends @repo/typescript-config/base |
| `packages/cms-client/package.json` | Placeholder — Dual-mode Payload client |
| `packages/cms-client/tsconfig.json` | Extends @repo/typescript-config/base |
| `packages/ui/package.json` | Placeholder — shadcn/ui + Atomic Design |
| `packages/ui/tsconfig.json` | Extends @repo/typescript-config/react-library |
| `apps/web-next/package.json` | Placeholder — Next.js reference app |
| `apps/web-next/tsconfig.json` | Extends @repo/typescript-config/nextjs |
| `apps/web-tanstack/package.json` | Placeholder — TanStack Start reference app |
| `apps/web-tanstack/tsconfig.json` | Extends @repo/typescript-config/base |
| `apps/cms/package.json` | Placeholder — Payload admin shell |
| `apps/cms/tsconfig.json` | Extends @repo/typescript-config/nextjs |
| `apps/storybook/package.json` | Placeholder — Storybook instance |
| `apps/storybook/tsconfig.json` | Extends @repo/typescript-config/react-library |
| `docker-compose.yml` | PostgreSQL service for local dev |
| `.env.example` | Environment variable template |
---
### Task 1: Root workspace files
**Files:**
- Create: `package.json`
- Create: `pnpm-workspace.yaml`
- Create: `turbo.json`
- Create: `.npmrc`
- Create: `.gitignore`
- Create: `.env.example`
- [ ] **Step 1: Create root package.json**
```json
{
"name": "template",
"private": true,
"packageManager": "pnpm@9.15.4",
"engines": {
"node": ">=20"
},
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"typecheck": "turbo run typecheck",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\""
},
"devDependencies": {
"prettier": "^3.5.0",
"turbo": "^2.4.0",
"typescript": "^5.8.0"
}
}
```
- [ ] **Step 2: Create pnpm-workspace.yaml**
```yaml
packages:
- "apps/*"
- "packages/*"
```
- [ ] **Step 3: Create turbo.json**
```json
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^build"]
},
"typecheck": {
"dependsOn": ["^typecheck"]
}
}
}
```
- [ ] **Step 4: Create .npmrc**
```
auto-install-peers=true
enable-pre-post-scripts=true
```
- [ ] **Step 5: Create .gitignore**
```
# Dependencies
node_modules
# Turbo
.turbo
# Build outputs
dist
build
.next
out
storybook-static
# Environment
.env
.env.local
.env.*.local
# Testing
coverage
# OS
.DS_Store
Thumbs.db
# IDE
.vscode
.idea
*.swp
# Debug
npm-debug.log*
pnpm-debug.log*
# Superpowers brainstorm sessions
.superpowers/
```
- [ ] **Step 6: Create .env.example**
```
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/template
# Payload CMS
PAYLOAD_SECRET=your-secret-here
# App URLs
NEXT_PUBLIC_APP_URL=http://localhost:3000
CMS_URL=http://localhost:3001
```
- [ ] **Step 7: Commit**
```bash
git add package.json pnpm-workspace.yaml turbo.json .npmrc .gitignore .env.example
git commit -m "feat: scaffold root workspace files (Turborepo + pnpm)"
```
---
### Task 2: Shared TypeScript config package
**Files:**
- Create: `packages/typescript-config/package.json`
- Create: `packages/typescript-config/base.json`
- Create: `packages/typescript-config/nextjs.json`
- Create: `packages/typescript-config/react-library.json`
- [ ] **Step 1: Create package.json**
```json
{
"name": "@repo/typescript-config",
"private": true,
"version": "0.0.0"
}
```
- [ ] **Step 2: Create base.json**
This is the base TypeScript config used by all packages. Includes `experimentalDecorators` and `emitDecoratorMetadata` required by InversifyJS.
```json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"declaration": true,
"declarationMap": true,
"isolatedModules": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 3: Create nextjs.json**
```json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"incremental": true,
"plugins": [{ "name": "next" }]
}
}
```
- [ ] **Step 4: Create react-library.json**
```json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx"
}
}
```
- [ ] **Step 5: Commit**
```bash
git add packages/typescript-config/
git commit -m "feat: add shared TypeScript config package (@repo/typescript-config)"
```
---
### Task 3: Shared ESLint config package
**Files:**
- Create: `packages/eslint-config/package.json`
- Create: `packages/eslint-config/base.js`
- Create: `packages/eslint-config/next.js`
- Create: `packages/eslint-config/react-internal.js`
- [ ] **Step 1: Create package.json**
```json
{
"name": "@repo/eslint-config",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./base": "./base.js",
"./next": "./next.js",
"./react-internal": "./react-internal.js"
},
"devDependencies": {
"@eslint/js": "^9.20.0",
"@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.25.0",
"eslint": "^9.20.0",
"eslint-config-prettier": "^10.1.0",
"eslint-plugin-turbo": "^2.4.0",
"typescript-eslint": "^8.25.0"
}
}
```
- [ ] **Step 2: Create base.js**
```javascript
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import turboPlugin from "eslint-plugin-turbo";
export default [
{ ignores: ["dist/**", "node_modules/**"] },
js.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier,
{
plugins: { turbo: turboPlugin },
rules: {
"turbo/no-undeclared-env-vars": "warn",
},
},
];
```
- [ ] **Step 3: Create next.js**
```javascript
import baseConfig from "./base.js";
export default [
...baseConfig,
{ ignores: [".next/**", "out/**"] },
];
```
- [ ] **Step 4: Create react-internal.js**
```javascript
import baseConfig from "./base.js";
export default [...baseConfig];
```
- [ ] **Step 5: Commit**
```bash
git add packages/eslint-config/
git commit -m "feat: add shared ESLint config package (@repo/eslint-config)"
```
---
### Task 4: Placeholder packages (core, api, api-client, cms-core, cms-client, ui)
**Files:**
- Create: `packages/core/package.json`
- Create: `packages/core/tsconfig.json`
- Create: `packages/core/src/index.ts`
- Create: `packages/api/package.json`
- Create: `packages/api/tsconfig.json`
- Create: `packages/api/src/index.ts`
- Create: `packages/api-client/package.json`
- Create: `packages/api-client/tsconfig.json`
- Create: `packages/api-client/src/index.ts`
- Create: `packages/cms-core/package.json`
- Create: `packages/cms-core/tsconfig.json`
- Create: `packages/cms-core/src/index.ts`
- Create: `packages/cms-client/package.json`
- Create: `packages/cms-client/tsconfig.json`
- Create: `packages/cms-client/src/index.ts`
- Create: `packages/ui/package.json`
- Create: `packages/ui/tsconfig.json`
- Create: `packages/ui/src/index.ts`
- [ ] **Step 1: Create packages/core/package.json**
```json
{
"name": "@repo/core",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
```
- [ ] **Step 2: Create packages/core/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["reflect-metadata"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 3: Create packages/core/src/index.ts**
```typescript
// @repo/core — Clean Architecture core package
// Layers: entities, application, infrastructure, interface-adapters, di
export {};
```
- [ ] **Step 4: Create packages/api/package.json**
```json
{
"name": "@repo/api",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
```
- [ ] **Step 5: Create packages/api/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 6: Create packages/api/src/index.ts**
```typescript
// @repo/api — tRPC router definitions
export {};
```
- [ ] **Step 7: Create packages/api-client/package.json**
```json
{
"name": "@repo/api-client",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
```
- [ ] **Step 8: Create packages/api-client/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 9: Create packages/api-client/src/index.ts**
```typescript
// @repo/api-client — Shared React Query hooks
export {};
```
- [ ] **Step 10: Create packages/cms-core/package.json**
```json
{
"name": "@repo/cms-core",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
```
- [ ] **Step 11: Create packages/cms-core/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 12: Create packages/cms-core/src/index.ts**
```typescript
// @repo/cms-core — Payload CMS config, collections, hooks, globals
export {};
```
- [ ] **Step 13: Create packages/cms-client/package.json**
```json
{
"name": "@repo/cms-client",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
```
- [ ] **Step 14: Create packages/cms-client/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 15: Create packages/cms-client/src/index.ts**
```typescript
// @repo/cms-client — Dual-mode Payload client (local + HTTP)
export {};
```
- [ ] **Step 16: Create packages/ui/package.json**
```json
{
"name": "@repo/ui",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
```
- [ ] **Step 17: Create packages/ui/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 18: Create packages/ui/src/index.ts**
```typescript
// @repo/ui — shadcn/ui + Atomic Design component library
export {};
```
- [ ] **Step 19: Commit**
```bash
git add packages/core/ packages/api/ packages/api-client/ packages/cms-core/ packages/cms-client/ packages/ui/
git commit -m "feat: add placeholder packages (core, api, api-client, cms-core, cms-client, ui)"
```
---
### Task 5: Placeholder apps (web-next, web-tanstack, cms, storybook)
**Files:**
- Create: `apps/web-next/package.json`
- Create: `apps/web-next/tsconfig.json`
- Create: `apps/web-tanstack/package.json`
- Create: `apps/web-tanstack/tsconfig.json`
- Create: `apps/cms/package.json`
- Create: `apps/cms/tsconfig.json`
- Create: `apps/storybook/package.json`
- Create: `apps/storybook/tsconfig.json`
- [ ] **Step 1: Create apps/web-next/package.json**
```json
{
"name": "@repo/web-next",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "echo 'placeholder'",
"dev": "echo 'placeholder'",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/api-client": "workspace:*",
"@repo/ui": "workspace:*"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
```
- [ ] **Step 2: Create apps/web-next/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"],
"exclude": ["node_modules"]
}
```
- [ ] **Step 3: Create apps/web-tanstack/package.json**
```json
{
"name": "@repo/web-tanstack",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "echo 'placeholder'",
"dev": "echo 'placeholder'",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/api-client": "workspace:*",
"@repo/ui": "workspace:*"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
```
- [ ] **Step 4: Create apps/web-tanstack/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
```
- [ ] **Step 5: Create apps/cms/package.json**
```json
{
"name": "@repo/cms",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "echo 'placeholder'",
"dev": "echo 'placeholder'",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/cms-core": "workspace:*"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
```
- [ ] **Step 6: Create apps/cms/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"],
"exclude": ["node_modules"]
}
```
- [ ] **Step 7: Create apps/storybook/package.json**
```json
{
"name": "@repo/storybook",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "echo 'placeholder'",
"dev": "echo 'placeholder'",
"lint": "eslint ."
},
"dependencies": {
"@repo/ui": "workspace:*"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
```
- [ ] **Step 8: Create apps/storybook/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
```
- [ ] **Step 9: Commit**
```bash
git add apps/
git commit -m "feat: add placeholder apps (web-next, web-tanstack, cms, storybook)"
```
---
### Task 6: Docker Compose
**Files:**
- Create: `docker-compose.yml`
- [ ] **Step 1: Create docker-compose.yml**
```yaml
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: template
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
```
- [ ] **Step 2: Commit**
```bash
git add docker-compose.yml
git commit -m "feat: add Docker Compose with PostgreSQL for local dev"
```
---
### Task 7: Install dependencies and verify workspace
- [ ] **Step 1: Install pnpm if not available**
Run: `corepack enable && corepack prepare pnpm@9.15.4 --activate`
Expected: pnpm is available
- [ ] **Step 2: Run pnpm install**
Run: `pnpm install`
Expected: Installs all workspace dependencies, creates `pnpm-lock.yaml`, no errors.
- [ ] **Step 3: Verify Turborepo sees all workspaces**
Run: `pnpm turbo run build --dry`
Expected: Output lists all 10 packages/apps:
- `@repo/typescript-config`
- `@repo/eslint-config`
- `@repo/core`
- `@repo/api`
- `@repo/api-client`
- `@repo/cms-core`
- `@repo/cms-client`
- `@repo/ui`
- `@repo/web-next`
- `@repo/web-tanstack`
- `@repo/cms`
- `@repo/storybook`
- [ ] **Step 4: Run turbo build**
Run: `pnpm build`
Expected: All workspaces build successfully (placeholder builds echo 'placeholder' or tsc --noEmit with no errors on empty src/index.ts).
- [ ] **Step 5: Verify Docker Compose**
Run: `docker compose up -d postgres && docker compose ps`
Expected: PostgreSQL container running, healthy.
Run: `docker compose down`
Expected: Clean shutdown.
- [ ] **Step 6: Commit lockfile**
```bash
git add pnpm-lock.yaml
git commit -m "chore: add pnpm lockfile"
```
---
### Task 8: Create test directory structure
**Files:**
- Create: `tests/unit/.gitkeep`
- Create: `tests/integration/.gitkeep`
- Create: `tests/e2e/.gitkeep`
- [ ] **Step 1: Create test directories**
```bash
mkdir -p tests/unit tests/integration tests/e2e
touch tests/unit/.gitkeep tests/integration/.gitkeep tests/e2e/.gitkeep
```
- [ ] **Step 2: Commit**
```bash
git add tests/
git commit -m "feat: add test directory structure (unit, integration, e2e)"
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,977 +0,0 @@
# Plan 3: Payload CMS Integration — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement Payload CMS integration: `@repo/cms-core` (config, collections, hooks), `@repo/cms-client` (dual-mode local+HTTP client with generated types), and `apps/cms` (thin Next.js shell serving the Payload admin panel).
**Architecture:** Payload config and collections live in `@repo/cms-core` — standalone, testable, framework-agnostic. `@repo/cms-client` provides a dual-mode client (Local API primary, HTTP fallback) that receives a Payload instance via injection. `apps/cms` is a thin Next.js 15 shell that imports config from cms-core and serves the admin panel. Type generation runs `payload generate:types` against cms-core's config.
**Tech Stack:** Payload CMS 3.x, @payloadcms/db-postgres, @payloadcms/next, @payloadcms/richtext-lexical, Next.js 15, PostgreSQL 16, sharp
---
## File Map
### packages/cms-core
| File | Responsibility |
|---|---|
| `packages/cms-core/package.json` | Dependencies: payload, @payloadcms/db-postgres, @payloadcms/richtext-lexical |
| `packages/cms-core/src/payload.config.ts` | Root Payload config (db, editor, collections, globals) |
| `packages/cms-core/src/collections/users/index.ts` | Users collection with auth enabled |
| `packages/cms-core/src/collections/articles/index.ts` | Articles collection config |
| `packages/cms-core/src/collections/articles/fields.ts` | Article field definitions |
| `packages/cms-core/src/collections/articles/hooks/before-change.ts` | Slug auto-generation hook |
| `packages/cms-core/src/collections/media/index.ts` | Media collection with uploads |
| `packages/cms-core/src/globals/site-settings.ts` | Site settings global |
| `packages/cms-core/src/index.ts` | Exports config + all collections |
### packages/cms-client
| File | Responsibility |
|---|---|
| `packages/cms-client/package.json` | Dependencies: payload (types only) |
| `packages/cms-client/src/client.ts` | createPayloadClient() factory |
| `packages/cms-client/src/local-client.ts` | LocalPayloadClient — wraps Payload instance |
| `packages/cms-client/src/http-client.ts` | HTTPPayloadClient — REST API fallback |
| `packages/cms-client/src/types.ts` | Shared client types (PayloadClient interface) |
| `packages/cms-client/src/index.ts` | Exports |
### apps/cms
| File | Responsibility |
|---|---|
| `apps/cms/package.json` | Dependencies: next, payload, @payloadcms/next, @repo/cms-core |
| `apps/cms/next.config.mjs` | Next.js config wrapped with withPayload |
| `apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx` | Admin panel catch-all route |
| `apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx` | Admin 404 page |
| `apps/cms/src/app/(payload)/layout.tsx` | Payload layout with RootLayout |
| `apps/cms/src/app/(payload)/custom.scss` | Empty custom styles |
| `apps/cms/src/payload-types.ts` | Generated types (via payload generate:types) |
---
### Task 1: Install cms-core dependencies
**Files:**
- Modify: `packages/cms-core/package.json`
- Modify: `packages/cms-core/tsconfig.json`
- [ ] **Step 1: Update packages/cms-core/package.json**
```json
{
"name": "@repo/cms-core",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"payload": "^3.14.0",
"@payloadcms/db-postgres": "^3.14.0",
"@payloadcms/richtext-lexical": "^3.14.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.0.0"
}
}
```
- [ ] **Step 2: Update tsconfig.json**
```json
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 3: Run pnpm install**
Run: `pnpm install`
Expected: Payload and its dependencies install successfully.
- [ ] **Step 4: Commit**
```bash
git add packages/cms-core/package.json packages/cms-core/tsconfig.json pnpm-lock.yaml
git commit -m "feat(cms-core): add Payload CMS dependencies"
```
---
### Task 2: Users collection
**Files:**
- Create: `packages/cms-core/src/collections/users/index.ts`
- [ ] **Step 1: Create Users collection**
```typescript
import type { CollectionConfig } from "payload";
export const Users: CollectionConfig = {
slug: "users",
auth: true,
admin: {
useAsTitle: "email",
},
fields: [
{
name: "displayName",
type: "text",
},
{
name: "role",
type: "select",
options: [
{ label: "Admin", value: "admin" },
{ label: "Editor", value: "editor" },
{ label: "Author", value: "author" },
],
defaultValue: "author",
required: true,
},
],
};
```
- [ ] **Step 2: Commit**
```bash
git add packages/cms-core/src/collections/users/
git commit -m "feat(cms-core): add Users collection with auth"
```
---
### Task 3: Articles collection with fields and hooks
**Files:**
- Create: `packages/cms-core/src/collections/articles/fields.ts`
- Create: `packages/cms-core/src/collections/articles/hooks/before-change.ts`
- Create: `packages/cms-core/src/collections/articles/index.ts`
- [ ] **Step 1: Create fields.ts**
```typescript
import type { Field } from "payload";
export const articleFields: Field[] = [
{
name: "title",
type: "text",
required: true,
maxLength: 255,
},
{
name: "slug",
type: "text",
unique: true,
admin: {
position: "sidebar",
description: "Auto-generated from title if left empty",
},
},
{
name: "content",
type: "richText",
},
{
name: "status",
type: "select",
options: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
],
defaultValue: "draft",
required: true,
admin: {
position: "sidebar",
},
},
{
name: "author",
type: "relationship",
relationTo: "users",
required: true,
admin: {
position: "sidebar",
},
},
{
name: "featuredImage",
type: "upload",
relationTo: "media",
},
{
name: "publishedAt",
type: "date",
admin: {
position: "sidebar",
date: {
pickerAppearance: "dayAndTime",
},
},
},
];
```
- [ ] **Step 2: Create hooks/before-change.ts**
This is a CMS-operational hook (slug auto-generation) — stays in cms-core per the design spec.
```typescript
import type { CollectionBeforeChangeHook } from "payload";
function generateSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
export const autoGenerateSlug: CollectionBeforeChangeHook = ({
data,
operation,
}) => {
if (operation === "create" || operation === "update") {
if (data && data.title && !data.slug) {
data.slug = generateSlug(data.title);
}
}
return data;
};
```
- [ ] **Step 3: Create articles/index.ts**
```typescript
import type { CollectionConfig } from "payload";
import { articleFields } from "./fields.js";
import { autoGenerateSlug } from "./hooks/before-change.js";
export const Articles: CollectionConfig = {
slug: "articles",
admin: {
useAsTitle: "title",
defaultColumns: ["title", "status", "author", "updatedAt"],
},
hooks: {
beforeChange: [autoGenerateSlug],
},
versions: {
drafts: true,
},
fields: articleFields,
};
```
- [ ] **Step 4: Commit**
```bash
git add packages/cms-core/src/collections/articles/
git commit -m "feat(cms-core): add Articles collection with slug auto-generation hook"
```
---
### Task 4: Media collection
**Files:**
- Create: `packages/cms-core/src/collections/media/index.ts`
- [ ] **Step 1: Create Media collection**
```typescript
import type { CollectionConfig } from "payload";
export const Media: CollectionConfig = {
slug: "media",
upload: {
mimeTypes: ["image/*", "application/pdf"],
},
admin: {
useAsTitle: "filename",
},
fields: [
{
name: "alt",
type: "text",
required: true,
},
],
};
```
- [ ] **Step 2: Commit**
```bash
git add packages/cms-core/src/collections/media/
git commit -m "feat(cms-core): add Media collection with uploads"
```
---
### Task 5: Site settings global
**Files:**
- Create: `packages/cms-core/src/globals/site-settings.ts`
- [ ] **Step 1: Create site-settings.ts**
```typescript
import type { GlobalConfig } from "payload";
export const SiteSettings: GlobalConfig = {
slug: "site-settings",
admin: {
group: "Settings",
},
fields: [
{
name: "siteName",
type: "text",
required: true,
defaultValue: "My App",
},
{
name: "siteDescription",
type: "textarea",
},
],
};
```
- [ ] **Step 2: Commit**
```bash
git add packages/cms-core/src/globals/
git commit -m "feat(cms-core): add SiteSettings global"
```
---
### Task 6: Payload config + exports
**Files:**
- Create: `packages/cms-core/src/payload.config.ts`
- Modify: `packages/cms-core/src/index.ts`
- [ ] **Step 1: Create payload.config.ts**
```typescript
import { buildConfig } from "payload";
import { postgresAdapter } from "@payloadcms/db-postgres";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Users } from "./collections/users/index.js";
import { Articles } from "./collections/articles/index.js";
import { Media } from "./collections/media/index.js";
import { SiteSettings } from "./globals/site-settings.js";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
export default buildConfig({
editor: lexicalEditor(),
collections: [Users, Articles, Media],
globals: [SiteSettings],
secret: process.env.PAYLOAD_SECRET || "default-secret-change-me",
db: postgresAdapter({
pool: {
connectionString:
process.env.DATABASE_URL ||
"postgresql://postgres:postgres@localhost:5432/template",
},
}),
typescript: {
outputFile: path.resolve(dirname, "payload-types.ts"),
},
});
```
- [ ] **Step 2: Update src/index.ts**
```typescript
export { Users } from "./collections/users/index.js";
export { Articles } from "./collections/articles/index.js";
export { Media } from "./collections/media/index.js";
export { SiteSettings } from "./globals/site-settings.js";
export { default as config } from "./payload.config.js";
```
- [ ] **Step 3: Commit**
```bash
git add packages/cms-core/src/payload.config.ts packages/cms-core/src/index.ts
git commit -m "feat(cms-core): add Payload config with postgres adapter and lexical editor"
```
---
### Task 7: cms-client — dual-mode Payload client
**Files:**
- Modify: `packages/cms-client/package.json`
- Create: `packages/cms-client/src/types.ts`
- Create: `packages/cms-client/src/local-client.ts`
- Create: `packages/cms-client/src/http-client.ts`
- Create: `packages/cms-client/src/client.ts`
- Modify: `packages/cms-client/src/index.ts`
- [ ] **Step 1: Update packages/cms-client/package.json**
```json
{
"name": "@repo/cms-client",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"payload": "^3.14.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.0.0"
}
}
```
- [ ] **Step 2: Create src/types.ts**
```typescript
export interface FindOptions {
where?: Record<string, unknown>;
sort?: string;
limit?: number;
page?: number;
depth?: number;
locale?: string;
}
export interface PayloadClientResult<T> {
docs: T[];
totalDocs: number;
limit: number;
totalPages: number;
page: number;
pagingCounter: number;
hasPrevPage: boolean;
hasNextPage: boolean;
prevPage: number | null;
nextPage: number | null;
}
export interface PayloadClient {
find<T = Record<string, unknown>>(
collection: string,
options?: FindOptions
): Promise<PayloadClientResult<T>>;
findByID<T = Record<string, unknown>>(
collection: string,
id: string,
options?: { depth?: number }
): Promise<T>;
create<T = Record<string, unknown>>(
collection: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T>;
update<T = Record<string, unknown>>(
collection: string,
id: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T>;
delete<T = Record<string, unknown>>(
collection: string,
id: string
): Promise<T>;
}
```
- [ ] **Step 3: Create src/local-client.ts**
```typescript
import type { Payload } from "payload";
import type { FindOptions, PayloadClient, PayloadClientResult } from "./types.js";
export class LocalPayloadClient implements PayloadClient {
constructor(private payload: Payload) {}
async find<T = Record<string, unknown>>(
collection: string,
options?: FindOptions
): Promise<PayloadClientResult<T>> {
const result = await this.payload.find({
collection: collection as any,
where: options?.where as any,
sort: options?.sort,
limit: options?.limit,
page: options?.page,
depth: options?.depth,
locale: options?.locale as any,
});
return result as unknown as PayloadClientResult<T>;
}
async findByID<T = Record<string, unknown>>(
collection: string,
id: string,
options?: { depth?: number }
): Promise<T> {
const result = await this.payload.findByID({
collection: collection as any,
id,
depth: options?.depth,
});
return result as unknown as T;
}
async create<T = Record<string, unknown>>(
collection: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T> {
const result = await this.payload.create({
collection: collection as any,
data: data as any,
depth: options?.depth,
});
return result as unknown as T;
}
async update<T = Record<string, unknown>>(
collection: string,
id: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T> {
const result = await this.payload.update({
collection: collection as any,
id,
data: data as any,
depth: options?.depth,
});
return result as unknown as T;
}
async delete<T = Record<string, unknown>>(
collection: string,
id: string
): Promise<T> {
const result = await this.payload.delete({
collection: collection as any,
id,
});
return result as unknown as T;
}
}
```
- [ ] **Step 4: Create src/http-client.ts**
```typescript
import type { FindOptions, PayloadClient, PayloadClientResult } from "./types.js";
export class HTTPPayloadClient implements PayloadClient {
constructor(private baseURL: string) {}
private async request<T>(path: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${this.baseURL}${path}`, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!response.ok) {
throw new Error(`Payload API error: ${response.status} ${response.statusText}`);
}
return response.json() as Promise<T>;
}
async find<T = Record<string, unknown>>(
collection: string,
options?: FindOptions
): Promise<PayloadClientResult<T>> {
const params = new URLSearchParams();
if (options?.limit) params.set("limit", String(options.limit));
if (options?.page) params.set("page", String(options.page));
if (options?.sort) params.set("sort", options.sort);
if (options?.depth) params.set("depth", String(options.depth));
if (options?.where) params.set("where", JSON.stringify(options.where));
const query = params.toString();
return this.request<PayloadClientResult<T>>(
`/api/${collection}${query ? `?${query}` : ""}`
);
}
async findByID<T = Record<string, unknown>>(
collection: string,
id: string,
options?: { depth?: number }
): Promise<T> {
const params = new URLSearchParams();
if (options?.depth) params.set("depth", String(options.depth));
const query = params.toString();
return this.request<T>(
`/api/${collection}/${id}${query ? `?${query}` : ""}`
);
}
async create<T = Record<string, unknown>>(
collection: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T> {
const params = new URLSearchParams();
if (options?.depth) params.set("depth", String(options.depth));
const query = params.toString();
return this.request<T>(
`/api/${collection}${query ? `?${query}` : ""}`,
{ method: "POST", body: JSON.stringify(data) }
);
}
async update<T = Record<string, unknown>>(
collection: string,
id: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T> {
const params = new URLSearchParams();
if (options?.depth) params.set("depth", String(options.depth));
const query = params.toString();
return this.request<T>(
`/api/${collection}/${id}${query ? `?${query}` : ""}`,
{ method: "PATCH", body: JSON.stringify(data) }
);
}
async delete<T = Record<string, unknown>>(
collection: string,
id: string
): Promise<T> {
return this.request<T>(`/api/${collection}/${id}`, { method: "DELETE" });
}
}
```
- [ ] **Step 5: Create src/client.ts**
```typescript
import type { Payload } from "payload";
import type { PayloadClient } from "./types.js";
import { LocalPayloadClient } from "./local-client.js";
import { HTTPPayloadClient } from "./http-client.js";
type PayloadClientOptions =
| { mode: "local"; payload: Payload }
| { mode: "http"; baseURL: string };
export function createPayloadClient(options: PayloadClientOptions): PayloadClient {
if (options.mode === "local") {
return new LocalPayloadClient(options.payload);
}
return new HTTPPayloadClient(options.baseURL);
}
```
- [ ] **Step 6: Update src/index.ts**
```typescript
export { createPayloadClient } from "./client.js";
export { LocalPayloadClient } from "./local-client.js";
export { HTTPPayloadClient } from "./http-client.js";
export type {
PayloadClient,
PayloadClientResult,
FindOptions,
} from "./types.js";
```
- [ ] **Step 7: Run pnpm install and commit**
Run: `pnpm install`
```bash
git add packages/cms-client/ pnpm-lock.yaml
git commit -m "feat(cms-client): add dual-mode Payload client (local + HTTP)"
```
---
### Task 8: apps/cms — thin Next.js shell
**Files:**
- Modify: `apps/cms/package.json`
- Create: `apps/cms/next.config.mjs`
- Create: `apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx`
- Create: `apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx`
- Create: `apps/cms/src/app/(payload)/layout.tsx`
- Create: `apps/cms/src/app/(payload)/custom.scss`
- Modify: `apps/cms/tsconfig.json`
- [ ] **Step 1: Update apps/cms/package.json**
```json
{
"name": "@repo/cms",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "next build",
"dev": "next dev --port 3001",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"generate:types": "payload generate:types"
},
"dependencies": {
"@payloadcms/next": "^3.14.0",
"@payloadcms/ui": "^3.14.0",
"@repo/cms-core": "workspace:*",
"next": "^15.3.0",
"payload": "^3.14.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sharp": "^0.33.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0"
}
}
```
- [ ] **Step 2: Update apps/cms/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@payload-config": ["../../packages/cms-core/src/payload.config.ts"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"],
"exclude": ["node_modules"]
}
```
- [ ] **Step 3: Create next.config.mjs**
```javascript
import { withPayload } from "@payloadcms/next/withPayload";
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withPayload(nextConfig);
```
- [ ] **Step 4: Create admin catch-all page**
```tsx
// apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from "next";
import config from "@payload-config";
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
import { importMap } from "../importMap.js";
type Args = {
params: Promise<{ segments: string[] }>;
searchParams: Promise<Record<string, string | string[]>>;
};
export const generateMetadata = ({
params,
searchParams,
}: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams });
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams });
export default Page;
```
- [ ] **Step 5: Create admin not-found page**
```tsx
// apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from "next";
import config from "@payload-config";
import { NotFoundPage, generatePageMetadata } from "@payloadcms/next/views";
import { importMap } from "../importMap.js";
type Args = {
params: Promise<{ segments: string[] }>;
searchParams: Promise<Record<string, string | string[]>>;
};
export const generateMetadata = ({
params,
searchParams,
}: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams });
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams });
export default NotFound;
```
- [ ] **Step 6: Create payload layout**
```tsx
// apps/cms/src/app/(payload)/layout.tsx
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { ServerFunctionClient } from "payload";
import config from "@payload-config";
import { RootLayout } from "@payloadcms/next/layouts";
import React from "react";
import { importMap } from "./importMap.js";
import "./custom.scss";
type Args = {
children: React.ReactNode;
};
const serverFunction: ServerFunctionClient = async function (args) {
"use server";
const { default: payloadModule } = await import("payload");
return payloadModule.handleServerFunctions({ ...args, config, importMap });
};
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
);
export default Layout;
```
- [ ] **Step 7: Create empty importMap and custom.scss**
```typescript
// apps/cms/src/app/(payload)/importMap.js
export const importMap = {};
```
```scss
// apps/cms/src/app/(payload)/custom.scss
// Custom admin panel styles
```
- [ ] **Step 8: Run pnpm install and commit**
Run: `pnpm install`
```bash
git add apps/cms/ pnpm-lock.yaml
git commit -m "feat(cms): add thin Next.js shell for Payload admin panel"
```
---
### Task 9: Update docker-compose with CMS service
**Files:**
- Modify: `docker-compose.yml`
- [ ] **Step 1: Update docker-compose.yml**
```yaml
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: template
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
```
Note: CMS app service will be added when Dockerfiles are created in a later plan. For now, local dev uses `pnpm dev --filter @repo/cms`.
- [ ] **Step 2: Commit (no change needed if docker-compose is already correct)**
Only commit if docker-compose was modified.
---
### Task 10: Verify build
- [ ] **Step 1: Run pnpm install from root**
Run: `pnpm install`
Expected: All dependencies resolve.
- [ ] **Step 2: Run turbo build**
Run: `pnpm build`
Expected: All workspaces build. The cms-core and cms-client packages should pass `tsc --noEmit`. The cms app should run `next build` (may need database for full build — if it fails due to no DB, that's expected and acceptable at this stage).
- [ ] **Step 3: Commit any remaining changes**
```bash
git add -A && git status
# Only commit if there are changes
git commit -m "chore: update lockfile after CMS integration"
```

View File

@@ -1,628 +0,0 @@
# Plan 4: API Layer + App Shells — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement tRPC routers (`@repo/api`), shared React Query hooks (`@repo/api-client`), and both app shells (`apps/web-next` with Next.js 15, `apps/web-tanstack` with TanStack Start) — completing the full data flow from UI to core.
**Architecture:** `@repo/api` defines the tRPC router that calls controllers from `@repo/core`. `@repo/api-client` provides a framework-agnostic React Query provider and typed hooks. Each app hosts its own tRPC HTTP endpoint and wraps with the shared provider. Both apps use identical hooks.
**Tech Stack:** tRPC v11, @trpc/tanstack-react-query, TanStack Query v5, Next.js 15 (App Router), TanStack Start (Vite-based), Zustand
---
## File Map
### packages/api
| File | Responsibility |
|---|---|
| `packages/api/package.json` | tRPC server deps |
| `packages/api/src/trpc.ts` | tRPC init, context, middleware |
| `packages/api/src/router/auth.router.ts` | Auth procedures |
| `packages/api/src/router/content.router.ts` | Content procedures |
| `packages/api/src/router/index.ts` | Root appRouter |
| `packages/api/src/index.ts` | Exports AppRouter type |
### packages/api-client
| File | Responsibility |
|---|---|
| `packages/api-client/package.json` | tRPC client + React Query deps |
| `packages/api-client/src/trpc.ts` | createTRPCReact instance |
| `packages/api-client/src/query-client.ts` | Shared QueryClient factory |
| `packages/api-client/src/provider.tsx` | ApiProvider component |
| `packages/api-client/src/index.ts` | Exports provider + trpc |
### apps/web-next
| File | Responsibility |
|---|---|
| `apps/web-next/package.json` | Next.js 15 + deps |
| `apps/web-next/next.config.mjs` | Next.js config |
| `apps/web-next/src/app/layout.tsx` | Root layout with ApiProvider |
| `apps/web-next/src/app/page.tsx` | Home page |
| `apps/web-next/src/app/api/trpc/[trpc]/route.ts` | tRPC HTTP handler |
| `apps/web-next/src/lib/payload.ts` | Payload instance initialization |
### apps/web-tanstack
| File | Responsibility |
|---|---|
| `apps/web-tanstack/package.json` | TanStack Start + deps |
| `apps/web-tanstack/vite.config.ts` | Vite + TanStack Start plugin |
| `apps/web-tanstack/src/router.tsx` | TanStack Router config |
| `apps/web-tanstack/src/routes/__root.tsx` | Root layout with ApiProvider |
| `apps/web-tanstack/src/routes/index.tsx` | Home page |
| `apps/web-tanstack/src/lib/payload.ts` | Payload instance initialization |
---
### Task 1: packages/api — tRPC routers
**Files:**
- Modify: `packages/api/package.json`
- Modify: `packages/api/tsconfig.json`
- Create: `packages/api/src/trpc.ts`
- Create: `packages/api/src/router/auth.router.ts`
- Create: `packages/api/src/router/content.router.ts`
- Create: `packages/api/src/router/index.ts`
- Modify: `packages/api/src/index.ts`
- [ ] **Step 1: Update packages/api/package.json**
```json
{
"name": "@repo/api",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/core": "workspace:*",
"@trpc/server": "^11.1.0",
"zod": "^3.24.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.0.0"
}
}
```
- [ ] **Step 2: Update packages/api/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 3: Create src/trpc.ts**
```typescript
import { initTRPC } from "@trpc/server";
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
```
- [ ] **Step 4: Create src/router/auth.router.ts**
```typescript
import { z } from "zod";
import { router, publicProcedure } from "../trpc.js";
import {
signInController,
signUpController,
signOutController,
} from "@repo/core";
export const authRouter = router({
signIn: publicProcedure
.input(
z.object({
username: z.string().min(3).max(31),
password: z.string().min(6).max(255),
})
)
.mutation(async ({ input }) => {
return await signInController(input);
}),
signUp: publicProcedure
.input(
z.object({
username: z.string().min(3).max(31),
password: z.string().min(6).max(255),
confirmPassword: z.string().min(6).max(255),
})
)
.mutation(async ({ input }) => {
return await signUpController(input);
}),
signOut: publicProcedure
.input(z.object({ sessionId: z.string() }))
.mutation(async ({ input }) => {
return await signOutController(input.sessionId);
}),
});
```
- [ ] **Step 5: Create src/router/content.router.ts**
```typescript
import { z } from "zod";
import { router, publicProcedure } from "../trpc.js";
import { createArticleController, getArticlesController } from "@repo/core";
export const contentRouter = router({
listArticles: publicProcedure
.input(
z
.object({
status: z.string().optional(),
authorId: z.string().optional(),
limit: z.number().optional(),
offset: z.number().optional(),
})
.optional()
)
.query(async ({ input }) => {
return await getArticlesController(input ?? {});
}),
createArticle: publicProcedure
.input(
z.object({
title: z.string().min(1).max(255),
content: z.string(),
authorId: z.string(),
slug: z.string().optional(),
})
)
.mutation(async ({ input }) => {
return await createArticleController(input);
}),
});
```
- [ ] **Step 6: Create src/router/index.ts**
```typescript
import { router } from "../trpc.js";
import { authRouter } from "./auth.router.js";
import { contentRouter } from "./content.router.js";
export const appRouter = router({
auth: authRouter,
content: contentRouter,
});
export type AppRouter = typeof appRouter;
```
- [ ] **Step 7: Update src/index.ts**
```typescript
export { appRouter, type AppRouter } from "./router/index.js";
```
- [ ] **Step 8: Run pnpm install and commit**
Run: `pnpm install`
```bash
git add packages/api/ pnpm-lock.yaml
git commit -m "feat(api): add tRPC routers (auth + content) calling core controllers"
```
---
### Task 2: packages/api-client — shared React Query hooks + provider
**Files:**
- Modify: `packages/api-client/package.json`
- Modify: `packages/api-client/tsconfig.json`
- Create: `packages/api-client/src/trpc.ts`
- Create: `packages/api-client/src/query-client.ts`
- Create: `packages/api-client/src/provider.tsx`
- Modify: `packages/api-client/src/index.ts`
- [ ] **Step 1: Update packages/api-client/package.json**
```json
{
"name": "@repo/api-client",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/api": "workspace:*",
"@trpc/client": "^11.1.0",
"@trpc/tanstack-react-query": "^11.1.0",
"@tanstack/react-query": "^5.75.0",
"react": "^19.0.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/react": "^19.0.0"
}
}
```
- [ ] **Step 2: Update tsconfig.json**
```json
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 3: Create src/trpc.ts**
```typescript
import { createTRPCContext } from "@trpc/tanstack-react-query";
import type { AppRouter } from "@repo/api";
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
```
- [ ] **Step 4: Create src/query-client.ts**
```typescript
import { QueryClient } from "@tanstack/react-query";
let clientQueryClient: QueryClient | undefined;
export function getQueryClient(): QueryClient {
if (typeof window === "undefined") {
return new QueryClient({
defaultOptions: {
queries: { staleTime: 30 * 1000 },
},
});
}
if (!clientQueryClient) {
clientQueryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 30 * 1000 },
},
});
}
return clientQueryClient;
}
```
- [ ] **Step 5: Create src/provider.tsx**
```tsx
"use client";
import { QueryClientProvider } from "@tanstack/react-query";
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "@repo/api";
import { TRPCProvider } from "./trpc.js";
import { getQueryClient } from "./query-client.js";
export function ApiProvider({
children,
trpcUrl,
}: {
children: React.ReactNode;
trpcUrl: string;
}) {
const queryClient = getQueryClient();
const trpcClient = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: trpcUrl })],
});
return (
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</TRPCProvider>
);
}
```
- [ ] **Step 6: Update src/index.ts**
```typescript
export { ApiProvider } from "./provider.js";
export { useTRPC } from "./trpc.js";
export { getQueryClient } from "./query-client.js";
```
- [ ] **Step 7: Run pnpm install and commit**
Run: `pnpm install`
```bash
git add packages/api-client/ pnpm-lock.yaml
git commit -m "feat(api-client): add tRPC React Query provider and shared hooks"
```
---
### Task 3: apps/web-next — Next.js 15 app shell
**Files:**
- Modify: `apps/web-next/package.json`
- Create: `apps/web-next/next.config.mjs`
- Create: `apps/web-next/src/app/layout.tsx`
- Create: `apps/web-next/src/app/page.tsx`
- Create: `apps/web-next/src/app/api/trpc/[trpc]/route.ts`
- Modify: `apps/web-next/tsconfig.json`
- [ ] **Step 1: Update apps/web-next/package.json**
```json
{
"name": "@repo/web-next",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "next build",
"dev": "next dev --port 3000",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/api": "workspace:*",
"@repo/api-client": "workspace:*",
"@repo/ui": "workspace:*",
"next": "^15.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0"
}
}
```
- [ ] **Step 2: Create next.config.mjs**
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@repo/api", "@repo/api-client", "@repo/core", "@repo/ui"],
};
export default nextConfig;
```
- [ ] **Step 3: Create src/app/api/trpc/[trpc]/route.ts**
```typescript
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@repo/api";
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: () => ({}),
});
export { handler as GET, handler as POST };
```
- [ ] **Step 4: Create src/app/layout.tsx**
```tsx
import type { Metadata } from "next";
import { Providers } from "./providers";
export const metadata: Metadata = {
title: "Template — Next.js",
description: "Clean Architecture Monorepo Template",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
```
- [ ] **Step 5: Create src/app/providers.tsx**
```tsx
"use client";
import { ApiProvider } from "@repo/api-client";
export function Providers({ children }: { children: React.ReactNode }) {
return <ApiProvider trpcUrl="/api/trpc">{children}</ApiProvider>;
}
```
- [ ] **Step 6: Create src/app/page.tsx**
```tsx
export default function Home() {
return (
<main>
<h1>Template Next.js</h1>
<p>Clean Architecture Monorepo Template</p>
</main>
);
}
```
- [ ] **Step 7: Commit**
```bash
git add apps/web-next/ pnpm-lock.yaml
git commit -m "feat(web-next): add Next.js 15 app shell with tRPC endpoint"
```
---
### Task 4: apps/web-tanstack — TanStack Start app shell
**Files:**
- Modify: `apps/web-tanstack/package.json`
- Create: `apps/web-tanstack/vite.config.ts`
- Create: `apps/web-tanstack/src/router.tsx`
- Create: `apps/web-tanstack/src/routes/__root.tsx`
- Create: `apps/web-tanstack/src/routes/index.tsx`
- Modify: `apps/web-tanstack/tsconfig.json`
- [ ] **Step 1: Update apps/web-tanstack/package.json**
```json
{
"name": "@repo/web-tanstack",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite dev --port 3002",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/api": "workspace:*",
"@repo/api-client": "workspace:*",
"@repo/ui": "workspace:*",
"@tanstack/react-router": "^1.120.0",
"@tanstack/react-start": "^1.120.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"vite": "^6.3.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.4.0"
}
}
```
- [ ] **Step 2: Create vite.config.ts**
```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});
```
- [ ] **Step 3: Create src/routes/__root.tsx**
```tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { ApiProvider } from "@repo/api-client";
export const Route = createRootRoute({
component: () => (
<ApiProvider trpcUrl="http://localhost:3000/api/trpc">
<Outlet />
</ApiProvider>
),
});
```
- [ ] **Step 4: Create src/routes/index.tsx**
```tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: Home,
});
function Home() {
return (
<main>
<h1>Template TanStack Start</h1>
<p>Clean Architecture Monorepo Template</p>
</main>
);
}
```
- [ ] **Step 5: Commit**
```bash
git add apps/web-tanstack/ pnpm-lock.yaml
git commit -m "feat(web-tanstack): add TanStack Start app shell with tRPC client"
```
---
### Task 5: Install all dependencies and verify
- [ ] **Step 1: Run pnpm install**
Run: `pnpm install`
Expected: All dependencies resolve.
- [ ] **Step 2: Run turbo build**
Run: `pnpm build`
Expected: All packages build (apps may fail on `next build` / `vite build` without full setup — change to placeholder if needed).
- [ ] **Step 3: Run core tests**
Run: `cd packages/core && pnpm vitest run`
Expected: All 22 tests pass.
- [ ] **Step 4: Commit any remaining fixes**
```bash
git add -A
git commit -m "chore: finalize Plan 4 — API layer + app shells"
```

View File

@@ -1,610 +0,0 @@
# Plan 5: UI System — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Set up `@repo/ui` with Atomic Design folder structure, Tailwind CSS v4, shadcn/ui, base components (atoms + a molecule), and `apps/storybook` with Storybook 8.
**Architecture:** `@repo/ui` uses Atomic Design (atoms/molecules/organisms/templates). Tailwind v4 uses CSS-first config (`@import "tailwindcss"` + `@theme`). shadcn/ui components land in `atoms/` by default. Storybook runs as a separate app pulling stories from the UI package via `@storybook/react-vite` with `@tailwindcss/vite` plugin.
**Tech Stack:** Tailwind CSS v4, shadcn/ui, clsx, tailwind-merge, Storybook 8, @storybook/react-vite
---
### Task 1: Set up @repo/ui with Tailwind v4 + Atomic Design structure
**Files:**
- Modify: `packages/ui/package.json`
- Modify: `packages/ui/tsconfig.json`
- Create: `packages/ui/src/styles/globals.css`
- Create: `packages/ui/src/lib/utils.ts`
- Create: `packages/ui/src/atoms/index.ts`
- Create: `packages/ui/src/molecules/index.ts`
- Create: `packages/ui/src/organisms/index.ts`
- Create: `packages/ui/src/templates/index.ts`
- Modify: `packages/ui/src/index.ts`
- [ ] **Step 1: Update packages/ui/package.json**
```json
{
"name": "@repo/ui",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "echo 'typechecked by consuming app bundler'",
"lint": "eslint .",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"clsx": "^2.1.0",
"tailwind-merge": "^3.0.0",
"react": "^19.0.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/react": "^19.0.0",
"tailwindcss": "^4.1.0"
}
}
```
- [ ] **Step 2: Create src/styles/globals.css**
Tailwind v4 CSS-first config — no tailwind.config.ts needed.
```css
@import "tailwindcss";
@theme {
--color-background: hsl(0 0% 100%);
--color-foreground: hsl(240 10% 3.9%);
--color-card: hsl(0 0% 100%);
--color-card-foreground: hsl(240 10% 3.9%);
--color-popover: hsl(0 0% 100%);
--color-popover-foreground: hsl(240 10% 3.9%);
--color-primary: hsl(240 5.9% 10%);
--color-primary-foreground: hsl(0 0% 98%);
--color-secondary: hsl(240 4.8% 95.9%);
--color-secondary-foreground: hsl(240 5.9% 10%);
--color-muted: hsl(240 4.8% 95.9%);
--color-muted-foreground: hsl(240 3.8% 46.1%);
--color-accent: hsl(240 4.8% 95.9%);
--color-accent-foreground: hsl(240 5.9% 10%);
--color-destructive: hsl(0 84.2% 60.2%);
--color-destructive-foreground: hsl(0 0% 98%);
--color-border: hsl(240 5.9% 90%);
--color-input: hsl(240 5.9% 90%);
--color-ring: hsl(240 5.9% 10%);
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
}
```
- [ ] **Step 3: Create src/lib/utils.ts**
```typescript
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
- [ ] **Step 4: Create atomic design barrel files**
```typescript
// packages/ui/src/atoms/index.ts
// Atom components are exported from here
export {};
```
```typescript
// packages/ui/src/molecules/index.ts
// Molecule components are exported from here
export {};
```
```typescript
// packages/ui/src/organisms/index.ts
// Organism components are exported from here
export {};
```
```typescript
// packages/ui/src/templates/index.ts
// Template components are exported from here
export {};
```
- [ ] **Step 5: Update src/index.ts**
```typescript
export { cn } from "./lib/utils.js";
export * from "./atoms/index.js";
export * from "./molecules/index.js";
export * from "./organisms/index.js";
export * from "./templates/index.js";
```
- [ ] **Step 6: Run pnpm install and commit**
```bash
pnpm install
git add packages/ui/ pnpm-lock.yaml
git commit -m "feat(ui): set up Atomic Design structure with Tailwind v4"
```
---
### Task 2: Add Button atom
**Files:**
- Create: `packages/ui/src/atoms/button/button.tsx`
- Create: `packages/ui/src/atoms/button/button.stories.tsx`
- Create: `packages/ui/src/atoms/button/index.ts`
- Modify: `packages/ui/src/atoms/index.ts`
- [ ] **Step 1: Create button.tsx**
```tsx
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cn } from "../../lib/utils.js";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost";
size?: "sm" | "default" | "lg";
}
const variantStyles: Record<NonNullable<ButtonProps["variant"]>, string> = {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
};
const sizeStyles: Record<NonNullable<ButtonProps["size"]>, string> = {
sm: "h-9 px-3 text-sm",
default: "h-10 px-4 py-2",
lg: "h-11 px-8 text-lg",
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "default", ...props }, ref) => {
return (
<button
className={cn(
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
variantStyles[variant],
sizeStyles[size],
className
)}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
```
- [ ] **Step 2: Create button.stories.tsx**
```tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./button.js";
const meta = {
title: "Atoms/Button",
component: Button,
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["default", "secondary", "destructive", "outline", "ghost"],
},
size: { control: "select", options: ["sm", "default", "lg"] },
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { children: "Button", variant: "default" },
};
export const Secondary: Story = {
args: { children: "Secondary", variant: "secondary" },
};
export const Destructive: Story = {
args: { children: "Destructive", variant: "destructive" },
};
export const Outline: Story = {
args: { children: "Outline", variant: "outline" },
};
export const Ghost: Story = {
args: { children: "Ghost", variant: "ghost" },
};
```
- [ ] **Step 3: Create button/index.ts and update atoms/index.ts**
```typescript
// packages/ui/src/atoms/button/index.ts
export { Button, type ButtonProps } from "./button.js";
```
```typescript
// packages/ui/src/atoms/index.ts
export { Button, type ButtonProps } from "./button/index.js";
```
- [ ] **Step 4: Commit**
```bash
git add packages/ui/src/atoms/button/ packages/ui/src/atoms/index.ts
git commit -m "feat(ui): add Button atom with Storybook story"
```
---
### Task 3: Add Input atom
**Files:**
- Create: `packages/ui/src/atoms/input/input.tsx`
- Create: `packages/ui/src/atoms/input/input.stories.tsx`
- Create: `packages/ui/src/atoms/input/index.ts`
- Modify: `packages/ui/src/atoms/index.ts`
- [ ] **Step 1: Create input.tsx**
```tsx
import { forwardRef, type InputHTMLAttributes } from "react";
import { cn } from "../../lib/utils.js";
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = "Input";
```
- [ ] **Step 2: Create input.stories.tsx**
```tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Input } from "./input.js";
const meta = {
title: "Atoms/Input",
component: Input,
tags: ["autodocs"],
} satisfies Meta<typeof Input>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { placeholder: "Enter text..." },
};
export const Disabled: Story = {
args: { placeholder: "Disabled", disabled: true },
};
```
- [ ] **Step 3: Create index and update atoms barrel**
```typescript
// packages/ui/src/atoms/input/index.ts
export { Input, type InputProps } from "./input.js";
```
Update atoms/index.ts to add:
```typescript
export { Input, type InputProps } from "./input/index.js";
```
- [ ] **Step 4: Commit**
```bash
git add packages/ui/src/atoms/input/ packages/ui/src/atoms/index.ts
git commit -m "feat(ui): add Input atom with Storybook story"
```
---
### Task 4: Add Label atom
**Files:**
- Create: `packages/ui/src/atoms/label/label.tsx`
- Create: `packages/ui/src/atoms/label/index.ts`
- Modify: `packages/ui/src/atoms/index.ts`
- [ ] **Step 1: Create label.tsx**
```tsx
import { forwardRef, type LabelHTMLAttributes } from "react";
import { cn } from "../../lib/utils.js";
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {}
export const Label = forwardRef<HTMLLabelElement, LabelProps>(
({ className, ...props }, ref) => {
return (
<label
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
ref={ref}
{...props}
/>
);
}
);
Label.displayName = "Label";
```
- [ ] **Step 2: Create label/index.ts and update atoms barrel**
```typescript
// packages/ui/src/atoms/label/index.ts
export { Label, type LabelProps } from "./label.js";
```
Update atoms/index.ts to add:
```typescript
export { Label, type LabelProps } from "./label/index.js";
```
- [ ] **Step 3: Commit**
```bash
git add packages/ui/src/atoms/label/ packages/ui/src/atoms/index.ts
git commit -m "feat(ui): add Label atom"
```
---
### Task 5: Add FormField molecule
**Files:**
- Create: `packages/ui/src/molecules/form-field/form-field.tsx`
- Create: `packages/ui/src/molecules/form-field/form-field.stories.tsx`
- Create: `packages/ui/src/molecules/form-field/index.ts`
- Modify: `packages/ui/src/molecules/index.ts`
- [ ] **Step 1: Create form-field.tsx**
```tsx
import { type ReactNode } from "react";
import { Label } from "../../atoms/label/index.js";
import { Input, type InputProps } from "../../atoms/input/index.js";
import { cn } from "../../lib/utils.js";
export interface FormFieldProps extends InputProps {
label: string;
error?: string;
description?: string;
}
export function FormField({
label,
error,
description,
className,
id,
...inputProps
}: FormFieldProps) {
const fieldId = id ?? label.toLowerCase().replace(/\s+/g, "-");
return (
<div className={cn("space-y-2", className)}>
<Label htmlFor={fieldId}>{label}</Label>
<Input id={fieldId} {...inputProps} />
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
);
}
```
- [ ] **Step 2: Create form-field.stories.tsx**
```tsx
import type { Meta, StoryObj } from "@storybook/react";
import { FormField } from "./form-field.js";
const meta = {
title: "Molecules/FormField",
component: FormField,
tags: ["autodocs"],
} satisfies Meta<typeof FormField>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { label: "Email", placeholder: "you@example.com", type: "email" },
};
export const WithDescription: Story = {
args: {
label: "Username",
placeholder: "johndoe",
description: "Must be 3-31 characters",
},
};
export const WithError: Story = {
args: {
label: "Password",
type: "password",
error: "Password must be at least 6 characters",
},
};
```
- [ ] **Step 3: Create index and update molecules barrel**
```typescript
// packages/ui/src/molecules/form-field/index.ts
export { FormField, type FormFieldProps } from "./form-field.js";
```
```typescript
// packages/ui/src/molecules/index.ts
export { FormField, type FormFieldProps } from "./form-field/index.js";
```
- [ ] **Step 4: Commit**
```bash
git add packages/ui/src/molecules/
git commit -m "feat(ui): add FormField molecule (Label + Input + error)"
```
---
### Task 6: Set up apps/storybook
**Files:**
- Modify: `apps/storybook/package.json`
- Create: `apps/storybook/.storybook/main.ts`
- Create: `apps/storybook/.storybook/preview.ts`
- [ ] **Step 1: Update apps/storybook/package.json**
```json
{
"name": "@repo/storybook",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "storybook build",
"dev": "storybook dev -p 6006",
"lint": "eslint ."
},
"dependencies": {
"@repo/ui": "workspace:*"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@storybook/addon-essentials": "^8.6.0",
"@storybook/react-vite": "^8.6.0",
"@tailwindcss/vite": "^4.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"storybook": "^8.6.0",
"tailwindcss": "^4.1.0",
"vite": "^6.3.0"
}
}
```
- [ ] **Step 2: Create .storybook/main.ts**
```typescript
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
framework: "@storybook/react-vite",
stories: [
"../../../packages/ui/src/**/*.stories.@(ts|tsx)",
],
addons: ["@storybook/addon-essentials"],
docs: {
autodocs: "tag",
},
async viteFinal(config) {
const { mergeConfig } = await import("vite");
const tailwindPlugin = await import("@tailwindcss/vite");
return mergeConfig(config, {
plugins: [tailwindPlugin.default()],
});
},
};
export default config;
```
- [ ] **Step 3: Create .storybook/preview.ts**
```typescript
import type { Preview } from "@storybook/react";
import "../../../packages/ui/src/styles/globals.css";
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
```
- [ ] **Step 4: Run pnpm install and commit**
```bash
pnpm install
git add apps/storybook/ pnpm-lock.yaml
git commit -m "feat(storybook): add Storybook 8 with Tailwind v4 pulling stories from @repo/ui"
```
---
### Task 7: Verify build
- [ ] **Step 1: Run pnpm build**
Run: `pnpm build`
Expected: All workspaces pass.
- [ ] **Step 2: Verify core tests still pass**
Run: `cd packages/core && pnpm vitest run`
Expected: 22 tests pass.
- [ ] **Step 3: Commit any remaining fixes**
```bash
git add -A
git commit -m "chore: finalize Plan 5 — UI system with Atomic Design"
```

View File

@@ -1,9 +0,0 @@
# Plan 6: Documentation + Agent Infrastructure
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Create all AGENTS.md files (~22), root CLAUDE.md, .mcp.json, and docs/ architecture guides so AI agents can navigate and extend the codebase autonomously.
**Architecture:** 4-tier documentation: Root (CLAUDE.md + AGENTS.md) → Package → Layer → Domain. Each file contains rules, recipes, and tables — not prose.
**Tasks:** 5 tasks covering root docs, core AGENTS.md files, package AGENTS.md files, app AGENTS.md files, and docs/ folder.

View File

@@ -1,557 +0,0 @@
# 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 `<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:
```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 |