# Architecture Overview A vertical-feature monorepo. Business capabilities are top-level packages; non-business foundations are `core-*`. ## Package map ``` packages/ # Must-have foundation (no business logic) core-shared/ Generic primitives — Payload field/block helpers, tRPC init/context, instrumentation interfaces, audit protocol + truncate-ip + AuditEntry shape, BindContext + protocol types for optional cross-cutting cores core-cms/ Composition only: assembles feature CMS exports into one Payload config core-api/ Composition only: aggregates feature tRPC routers into one appRouter # Optional cross-cutting cores (scaffold on demand via `pnpm turbo gen core-package `) core-trpc/ Frontend tRPC client + per-framework providers (Next.js, TanStack) core-ui/ Design-system primitives (atoms, molecules, generic organisms, templates) core-realtime/ Socket.IO server + broadcaster + handler registry (ADR-016) core-events/ In-memory + Payload-backed event bus + job queue (ADR-015) core-audit/ DPA-compliant audit logging — sinks, hook factories, eraseSubject (ADR-018) core-analytics/ Product analytics capture channel (ADR-024) core-consent/ Consent records + cookie-consent banner (ADR-025) core-dsr/ Data-subject-rights — export, delete, rectify, restrict (ADR-025) # Business capabilities auth/ Users + sign-in/sign-up/sign-out + session/cookie domain blog/ Articles collection + publishing flow media/ Media upload collection (skeleton; expand with optimization, CDN, etc.) marketing-pages/ Pages collection + SiteSettings global navigation/ Header global + menu items # Tooling core-eslint/ Shared ESLint flat config + conformance rules + boundary rules core-typescript/ Shared tsconfig + vitest base core-testing/ Factories, contract suites, recording test doubles ``` See `docs/architecture/template-tiers.md` for the must-have/optional split and the scaffold commands. ## Data flow ``` React component ↓ useQuery(trpc.blog.articleBySlug.queryOptions({slug})) ← @repo//ui (queries) HTTP /api/trpc ↓ tRPC procedure (xProcedure.input(xInputSchema)) ← integrations/api/router.ts ↓ xProcedure has defineErrorMiddleware applied ← integrations/api/procedures.ts ↓ container.get(SYMBOL) Controller factory (xInputSchema.safeParse) ← interface-adapters/controllers/.controller.ts ↓ (useCase) => async (input: unknown) => Promise Use case factory ← application/use-cases/.use-case.ts ↓ (deps) => async (input: XInput) => XOutput ↓ ends with xOutputSchema.parse(result) Repository implementation ← infrastructure/repositories/.repository.ts ↓ getPayload({ config }) Payload Local API → Postgres ↓ on throw: domain error → defineErrorMiddleware → TRPCError(code, cause) ↓ on success: controller's `function presenter(value: XOutput)` shapes the view ↓ tRPC response ``` Use cases and controllers are **factory functions** — they take their dependencies as arguments and return the callable. The container wires them via `.toDynamicValue((ctx) => factoryFn(ctx.container.get(...)))`. Each exports `export type I*UseCase = ReturnType` (and the analogous `I*Controller`) so consumers can depend on the type without importing the impl. Controllers are **one per use case** — no multi-method controller files. **Schemas live 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`. Controllers and tRPC procedures import the schema — never redefine it. The use case body validates its output via `xOutputSchema.parse(...)` before returning, so a misbehaving repository fails loudly at the layer that owns the contract. **Controllers reshape via a co-located presenter**: every non-void controller defines a top-level `function presenter(value: XOutput)` and returns `Promise>`. Identity is fine — `return value;` — but the function form is always present so adding a transform later is a one-line edit. Void controllers (e.g. `signOutController`, `deleteMediaController`) return `Promise` and skip the presenter. **Domain errors map to `TRPCError` per feature**: each feature owns `integrations/api/procedures.ts` exporting `xProcedure = t.procedure.use( defineErrorMiddleware([[Ctor, "TRPC_CODE"], ...]))`. Routers use `xProcedure` instead of bare `publicProcedure`. `core-shared` provides the `defineErrorMiddleware` factory but never enumerates a feature's errors — each feature passes its own constructors in. ## Three enforcement layers 1. **`package.json` deps** — only declare allowed deps 2. **`exports` map** — each feature exposes a small public surface (`.`, `./ui`, `./cms`, `./api`, `./di/bind-production`, `./di/bind-dev-seed`) 3. **Two parallel automated checks**: - **ESLint `eslint-plugin-boundaries`** (lint-time) — enforces boundary rules at linting - **Turborepo `boundaries`** (build-graph time) — validates entire workspace dependency graph, including transitive reaches Both use the same five-tag model; see "Five tags" section below. ## Five tags The workspace is organized into five mutually exclusive tags: - **app** (4 packages): `apps/cms`, `apps/web-next`, `apps/web-tanstack`, `apps/storybook` - **core-composition** (2 must-have): `packages/core-api`, `packages/core-cms`. Plus `packages/core-trpc` when scaffolded via `pnpm turbo gen core-package trpc` (optional). - **core** (1 must-have): `packages/core-shared`. Plus `core-ui`, `core-realtime`, `core-events`, `core-audit`, `core-analytics`, `core-consent`, `core-dsr` when scaffolded via `pnpm turbo gen core-package ` (optional). - **feature** (5 packages): `packages/auth`, `packages/blog`, `packages/media`, `packages/marketing-pages`, `packages/navigation` - **tooling** (3 packages): `packages/core-eslint`, `packages/core-typescript`, `packages/core-testing` See `docs/architecture/template-tiers.md` for the must-have/optional split and the scaffold commands. **Allowed dependency directions:** | Tag | May depend on | | ---------------- | --------------------------------------------- | | app | app, core, core-composition, feature, tooling | | core-composition | core, core-composition, feature, tooling | | core | core, core-composition, tooling | | feature | core, feature, tooling | | tooling | tooling | A feature may import another feature's **public exports** — its `@repo/` contract barrel (types, errors, schemas, event contracts). It must not reach another feature's internals (the `exports` map seals those), and cross-feature _behaviour_ still flows through `IEventBus` — a feature never imports and invokes another feature's use cases directly. **Composition exceptions:** - `core-api` may import from `@repo//api` subpath exports only - `core-cms` may import from `@repo//cms` subpath exports only - `core-trpc` reaches features transitively through `core-api`'s `AppRouter` type ## Per-feature DI containers Each feature owns its own InversifyJS `Container` + symbol table. No shared symbols, no cross-feature DI coupling. Tests rebind per feature without touching others. Apps call `bindProduction*(config)` per feature at boot to swap the default mock implementations for Payload-backed ones. ## Spec reference `docs/architecture/vertical-feature-spec.md` is the canonical design. ## Interactive explainers Four single-file HTML walkthroughs sit alongside this overview. They're self-contained (no build step) and best viewed locally in a browser. - [`data-flow-explainer.html`](./data-flow-explainer.html) — request and event flow through one feature, with the DI binding mode toggle and feature swap controls. - [`di-explainer.html`](./di-explainer.html) — the full container + symbols + binder lifecycle. - [`feature-conformance-explainer.html`](./feature-conformance-explainer.html) — companion to ADR-020-adjacent conformance design (also linked from `agent-first-workflow-and-conformance.md` and `runbook.md`). - [`audit-and-compliance-explainer.html`](./audit-and-compliance-explainer.html) — companion to ADR-018 (also linked from `dependency-flow.md` and `guides/audit-and-compliance.md`). The four pages cross-link to each other; entering any of them surfaces the others.