# Clean Architecture Monorepo Template ## Quick Start ```bash pnpm install # Install + auto-wire husky pre-commit hooks pnpm dev # Start all dev servers pnpm build # Build all packages pnpm test # Run all tests pnpm typecheck # TypeScript across all packages pnpm lint # ESLint (incl. 8 conformance/* rules) pnpm conformance # Cross-feature event closure pnpm fallow # Whole-codebase: dead exports, dupes, complexity pnpm fallow:audit # AI-change audit (run before commits) pnpm coverage:aggregate # Merge per-package lcovs -> coverage/lcov.info + summary.json (L2) pnpm coverage:diff # Cover-the-diff gate; JSON to stdout (L1, ADR-020) pnpm mutate # Stryker mutation testing on entities + use-cases (L3, on-demand) pnpm turbo boundaries # Workspace dependency graph pnpm work status # docs/work/ epic + story state pnpm work next # Next ready story pnpm work dispatch # Print next dispatch plan (use --execute to invoke sandcastle) pnpm turbo gen feature # Scaffold a new feature package pnpm turbo gen event # Scaffold an event contract or handler pnpm turbo gen job # Scaffold a background job pnpm turbo gen realtime # Scaffold a realtime channel or handler pnpm turbo gen core-package # Scaffold an optional core package pnpm turbo gen core-ui-component # Scaffold an atomic-design component docker compose up -d # Start PostgreSQL ``` **First time?** Read [`docs/guides/runbook.md`](./docs/guides/runbook.md) end-to-end. ## TDD ```bash pnpm test --watch --filter @repo/ # watch one feature pnpm test -- --coverage # full run with coverage pnpm test:stories # Storybook smoke tests pnpm test:e2e # Playwright e2e ``` See `docs/guides/tdd-workflow.md` for the full cycle. ## Project Overview Turborepo + pnpm monorepo organized by vertical features. Each feature (`auth`, `blog`, `media`, `marketing-pages`, `navigation`) owns its Clean Architecture layers. Must-have core packages (`core-shared`, `core-cms`, `core-api`) provide foundation; five optional core packages (`core-realtime`, `core-events`, `core-trpc`, `core-ui`, `core-audit`) scaffold on demand via `pnpm turbo gen core-package ` (see `docs/architecture/template-tiers.md`). Two tooling packages (`core-eslint`, `core-typescript`) provide shared configs. Workspace boundaries are enforced by ESLint (lint-time) and Turborepo (build-graph time). Supports Next.js and TanStack Start as frontend frameworks, Payload CMS for content management, and comprehensive agent-optimized documentation. ## Read First - `docs/glossary.md` — **Canonical vocabulary** for the monorepo. Resolves "what does X mean here?" for every cross-cutting term (feature, use case, manifest, conformance, slice, dispatch, etc.). Shared between humans and agents. - `AGENTS.md` — Package map, boundary rules, per-package conventions - `docs/architecture/overview.md` — High-level architecture and package responsibilities - `docs/architecture/vertical-feature-spec.md` — Design spec with rationale and decision log - `docs/guides/scaffolding-a-feature.md` — `turbo gen feature` reference (fast path; prefer this over the manual walkthrough) - `docs/guides/adding-a-feature.md` — End-to-end new feature walkthrough (manual path; for cases the generator's scope doesn't cover) - `docs/guides/events-and-jobs.md` — publish/consume/schedule cookbook (cross-feature events + background jobs; _requires `gen core-package events`_) - `docs/guides/realtime.md` — Socket.IO channels, broadcasts, handlers (_requires `gen core-package realtime`_) - `docs/guides/audit-and-compliance.md` — DPA-compliant audit logging cookbook (_requires `gen core-package audit`_) - `docs/guides/coverage.md` — 4-layer coverage cookbook (L0 vitest thresholds, L1 `pnpm coverage:diff`, L2 aggregate, L3 mutation; ADR-020) - `docs/guides/releasing.md` — release-please workflow: how Conventional Commits become tagged versions + per-package CHANGELOGs (ADR-021) - `docs/architecture/template-tiers.md` — must-have vs optional packages and how to scaffold the optionals ## Conformance system Every feature has a `src/feature.manifest.ts` declaring its use cases, audits, publishes, consumes, and required cores. Drift is caught at five latencies: | Layer | Latency | Catches | | -------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------- | | **TypeScript brands** | 0s | forgotten `withSpan` / `withCapture` / `withAudit` at bind time | | **ESLint rules** | <1s | manifest ↔ code drift; undeclared `bus.publish` / `auditLog.record`; missing manifest; missing sibling test | | **Boot assertion** (`pnpm dev`) | ~3s | binding without required brand at runtime; manifest edited without rebinder | | **CI drift gate** (`pnpm conformance`) | ~120s | orphan event consumers across features | | **Fallow** (`pnpm fallow`) | ~30–60s | dead exports / unused files; duplicate code; circular deps; complexity hotspots; AI-change audit drift | The five conformance ESLint rules: `feature-must-have-manifest` (error), `usecase-must-have-test-file` (error), `required-cores-installed` (error), `no-undeclared-event-publish` (warn), `no-undeclared-audit` (warn). Fallow runs as a fifth layer, post-ESLint, whole-codebase. See `docs/architecture/agent-first-workflow-and-conformance.md` for the full design and `docs/guides/conformance-quickref.md` for the day-to-day reference. ### Sibling architecture: coverage (ADR-020) Coverage runs in parallel to the 5-gate conformance system above — same multi-latency philosophy, different signal. Each feature's `feature.manifest.ts` declares a `coverage.bands` section that vitest (test-time), `pnpm coverage:diff` (CI/agent-loop), and `pnpm mutate` (nightly) all read from. Four layers: | Layer | Catches | Surface | | ---------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------- | | **L0** Per-layer vitest thresholds | Drift below declared bands (entities/use-cases/controllers at 100%) | `pnpm test -- --coverage` | | **L1** Diff coverage | Changed line not exercised by tests | `pnpm coverage:diff` — CI-gated on PRs + dispatch post-task | | **L2** Aggregate trend | Codebase coverage drifted over time | `pnpm coverage:aggregate` → committed `coverage/summary.json` | | **L3** Mutation testing | Tests that exist + execute the code but assert nothing | `pnpm mutate` — on-demand + nightly GH Action | See `docs/guides/coverage.md` for the cookbook and ADR-020 for the full rationale. Agents running in sandcastle: run `pnpm coverage:diff` before reporting `complete` — the implementer and reviewer prompts enforce this. ## Key Conventions - **Conventional Commits (non-negotiable)** — Every commit message MUST follow the [Conventional Commits](https://www.conventionalcommits.org/) spec: `(): ` (≤72 chars). Types: `feat | fix | docs | style | refactor | test | chore | perf | ci | build | revert`. Use `!` after type/scope for breaking changes. Body explains WHY if non-obvious. Examples: `feat(auth): hash password before persisting`, `test(blog): assert article not found error`, `refactor(docs)!: consolidate scaffolding into guides`. The sandcastle implementer + reviewer prompts both enforce this; agents authoring commits autonomously MUST honor it. Commits become versions + changelog entries automatically via release-please (ADR-021 / `docs/guides/releasing.md`). - **Versioning is hybrid (ADR-021)** — Root template (`template-vertical`) + 5 feature packages (`@repo/{auth,blog,media,marketing-pages,navigation}`) each version independently from `0.1.0`. release-please reads Conventional Commits since the last tag and opens a rolling release PR on every merge to main; merging it cuts per-package tags (`template-v0.2.0`, `auth-v0.1.1`, etc.) + GitHub releases. **Bump targeting is by commit path** — files under `packages//**` bump that feature; cross-cutting paths (`docs/`, `scripts/`, `.github/`, root configs) bump the root. Pre-1.0 policy: `feat:` → patch, `feat!:` → minor. - **Relative imports in `src/`** — Source files use relative paths (`../repositories/...`), not `@/` alias - **`@/` alias in tests** — Test files (`*.test.ts`) use `@/` to import from `src/` - **`vitest.config.ts`** — Every package must define `resolve.alias: { "@": path.resolve(__dirname, "./src") }` - **`tsconfig.json` rootDir** — Set `"rootDir": "."` so TypeScript finds both `src/` and test files - **File layout convention** — Entities live at `entities/models/.ts`; errors at `entities/errors/.ts` + `entities/errors/common.ts`; mock siblings use the `.mock.ts` suffix (`.repository.mock.ts`); real repository impls drop the `payload-` prefix (`.repository.ts`); interface filenames are dot-separated (`.repository.interface.ts`) - **Factory-function use cases & controllers** — Every use case and controller is `(deps) => async (input) => result`; each exports `export type I*UseCase = ReturnType` (and the analogous `I*Controller`); one controller per use case (no multi-method controllers) - **DI uses `.toDynamicValue()` for factories** — `bind(SYMBOL).toDynamicValue((ctx) => xUseCase(ctx.container.get(...)))`; mocks remain the default binding - **Tests inject mocks directly** — Construct `MockXRepository` and pass into the factory: `signInUseCase(mockUsers, mockAuth)(input)`. No container rebinding in unit tests - **Schemas in the use-case file** — Every use case exports `xInputSchema` (a `z.ZodObject` with `.strict()`; `z.object({}).strict()` for void inputs) and, for non-void use cases, `xOutputSchema`. Types: `XInput = z.infer` and `XOutput`. Use case body ends with `xOutputSchema.parse(result)` before returning (runtime guarantee against malformed repository data) - **Controllers receive `unknown` + presenter** — Controllers `safeParse(xInputSchema)` from the use-case file and throw `InputParseError` on failure. Non-void controllers define a top-level `function presenter(value: XOutput)` and return `Promise>` (identity is fine — `return value`); void controllers return `Promise` with no presenter - **Feature-scoped tRPC error mapping** — Each feature has `integrations/api/procedures.ts` exporting `xProcedure = t.procedure.use(defineErrorMiddleware([[Ctor, "TRPC_CODE"], ...]))` from `@repo/core-shared/trpc/define-error-middleware`. Routers use `xProcedure.input(xInputSchema)` — schemas are imported from the use-case file, never redefined inline. `core-shared` never enumerates feature error classes - **Public surface split** — Feature root (`.`) exports contracts only: types, errors, schemas, IUseCase / IController aliases, router type, constants. UI artifacts (query builders, components) live behind `./ui` (`src/ui/index.ts`). Apps import queries from `@repo//ui`, schemas/types from `@repo/` - **Payload repositories via constructor** — Feature packages receive Payload config at constructor time, not as a direct dependency - **Three binding modes per feature** — Each feature exports two binders: `./di/bind-production` (real Payload) and `./di/bind-dev-seed` (populated mock). The app's `bindAll()` dispatcher in `apps/web-next/src/server/bind-production.ts` picks one by env: `USE_DEV_SEED="true"` → dev seed; `NODE_ENV="production"` → production; otherwise → dev seed (developer default so `pnpm dev` boots without Payload). Dev seed lives in `src/__seeds__/dev.ts` as a lazy `buildDev()` function that uses the feature's existing factory - **Binders take a `ctx` arg from `core-shared/di`** — `bindProductionX(ctx: BindProductionContext)` for production binders; `bindDevSeedX(ctx: BindContext)` for dev-seed. Required fields: `tracer`, `logger`, plus `config` for production. Optional fields: `bus`, `queue`, `realtime`, `realtimeRegistry` (correspond to optional core packages — guard with `?.` or `if (bus) { ... }` when used; use-case signatures should accept the protocol type when they only need protocol methods, not the full concrete interface). Aggregator builds one ctx object and passes it to all feature binders - **App bootstrap** — Each app calls `bindAll()` from a server entry point (page server component, route handler) before resolving any feature controller. The dispatcher is idempotent - **Instrumentation lives in `core-shared/instrumentation/`** — Three interfaces (`ITracer`, `ILogger`, `IMetrics`), three implementation pairs (`Noop*`, `Otel*`, and `Recording*` from `core-testing`). The OTel SDK is the substrate; Sentry is wired as the exporter via `@sentry/opentelemetry`. Feature packages MUST NOT import `@opentelemetry/sdk-*` or `@sentry/*` directly (ESLint-enforced); the vendor-neutral `@opentelemetry/api` family is the import surface for advanced cases (ADR-017) - **Spans + capture composed at DI bind time** — Use cases + controllers wrapped via `withSpan(tracer, spanOpts, withCapture(logger, tags, factory(deps)))` inside `bind-production` / `bind-dev-seed`. `withSpan` is outermost so an errored span's timing reflects the capture-and-rethrow. Repository methods are different — they call `this.tracer.startSpan(...)` and `this.logger.captureException(...)` inline per method because they own per-call attributes - **Capture at throw sites only, with double-report guard** — Repos capture infra errors inline; use cases + controllers capture via `withCapture` at bind time; `defineErrorMiddleware` never captures. Each error gets a non-enumerable `__sentryReported` flag the first time it's captured; `withCapture`, `OtelLogger`, and `RecordingLogger` all bail if the flag is set, so a bubbled error surfaces exactly once with the inner-most layer's tags (helper at `core-shared/instrumentation/reported-flag.ts`) - **PII handling is non-negotiable** — `sendDefaultPii: false` everywhere (CI grep gate); replay default-masks all text/inputs/media (allowlist starts empty); `setUser({ id })` only — no email/username; server-side PII scrubbing happens at the OTel processor layer (`PiiScrubSpanProcessor` + `PiiScrubLogRecordProcessor`) before any exporter sees the data (ADR-017 §7) - **Three apps, three Sentry projects** — `WEB_NEXT_SENTRY_DSN`, `CMS_SENTRY_DSN`, `WEB_TANSTACK_SENTRY_DSN`. Browser DSNs use `NEXT_PUBLIC_` (web-next) and `VITE_` (web-tanstack) prefixes - **Instrumentation binding is orthogonal to repo binding** — `bindAll()`'s Rule 0 (DSN → OTel+Sentry vs Noop) is independent of `USE_DEV_SEED` / `NODE_ENV`. Run `pnpm dev` with `WEB_NEXT_SENTRY_DSN` set to test the integration locally - **Cross-feature events go through `IEventBus` (E0)** — In-feature reactions are direct use-case calls, not bus publishes. The bus is for _crossing_ feature boundaries (e.g. `auth` → `marketing-pages` welcome email) - **Event contracts are public; handlers are private (E1)** — Publisher's `events/.event.ts` is exported from the feature root barrel. Consumer's `events/handlers/on--.handler.ts` is never re-exported (ESLint-enforced via `core-eslint/rules/no-handler-reexport`) - **Jobs are for _deferred_ work, not abstraction (J0)** — Synchronous code stays synchronous. A job exists only when something must run off the request path (latency, retries, cron). Feature packages enqueue via `IJobQueue` only — direct `payload.jobs.queue()` is ESLint-blocked outside `core-shared/jobs/` - **Realtime is for state delivery, not for replacing tRPC (R0)** — Persistent request/response operations belong on tRPC procedures. Use realtime when the server needs to push without a request or the data is too high-frequency for HTTP - **Realtime channel descriptors are exported; handlers are private (R1)** — A feature's `realtime/.channel.ts` is re-exported from the root barrel; `realtime/handlers/*.handler.ts` is wired only in bind-\* files and never re-exported (ESLint-enforced via `no-realtime-handler-reexport`) - **`socket.io` lives in `@repo/core-realtime` only (R2)** — Feature packages MUST NOT import `socket.io` or `socket.io-client`. ESLint rule `no-direct-socket-io` enforces this; allowlist covers `core-realtime/src/socket-io-*.ts` and `apps/*/server.ts` - **Manifest-first ordering** — for any new use case, the workflow is **(1) manifest entry** → **(2) contracts** (`xInputSchema`, `xOutputSchema`, `IXUseCase`) → **(3) tests (red)** → **(4) implementation (green)**. The generator emits the manifest + a self-asserting `bind-production.ts` so new features are conformance-compliant by default - **Self-asserting `bindProductionX(ctx)`** — every feature's bind-production calls `assertFeatureConformance(container, manifest, symbols, ctx)` at its tail. `pnpm dev` refuses to boot on drift - **`pnpm conformance`** — cross-feature event-closure check; fails CI on orphan consumers ## MCP Servers Start Storybook before UI work: `pnpm dev --filter @repo/storybook` Storybook MCP available at `http://localhost:6006/mcp` — use `list-all-documentation` to discover existing components before creating new ones. ## Key Ports | Service | Port | | -------------- | ---- | | Next.js | 3000 | | Payload CMS | 3001 | | TanStack Start | 3002 | | PostgreSQL | 5432 | | Storybook | 6006 |