AGENTS.md now describes the Veect control plane (ADR-027/028/029,
docs/product/ authority table, glossary Veect-domain vocabulary), with
the package map, boundary tags, and per-package conventions verified
against the filesystem (auth + 11 core + 3 tooling packages;
web-next/cms/storybook apps) and code examples drawn from the real auth
feature. Adds warning notes for known generator staleness — the
release-please per-feature registration that would collide with the
root-only v* tag policy, the pre-shipped trace overwrite hazard (zod
incident, restored in e4a3b65), the trpc template's removed
@trpc/react-query dep — and records the accepted warn-severity lint
backlog (~93 findings).
Per story amendments, the same slice prunes the dead "web-tanstack"
member from core-shared's app-tag unions (bind-otel-instrumentation.ts,
sentry/init-client.ts; tests retargeted to "web-next") and fixes the
stale app/feature tag lists in docs/architecture/overview.md. A warning
comment mirrors the release-please note at the generator call site; no
functional generator changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
8.8 KiB
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 (all currently scaffolded; new ones via `pnpm turbo gen core-package <name>`)
core-trpc/ Frontend tRPC client + Next.js provider
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
(control-plane identity, ADR-027; the Veect features —
workspaces, projects, discovery, design-doc, editor —
land as sibling feature packages)
# 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.<feature>.<procedure>.queryOptions({...})) ← @repo/<feature>/ui (queries)
HTTP /api/trpc
↓
tRPC procedure (xProcedure.input(xInputSchema)) ← integrations/api/router.ts
↓ xProcedure has defineErrorMiddleware applied ← integrations/api/procedures.ts
↓ container.get<IXController>(SYMBOL)
Controller factory (xInputSchema.safeParse) ← interface-adapters/controllers/<verb-noun>.controller.ts
↓ (useCase) => async (input: unknown) => Promise<view>
Use case factory ← application/use-cases/<verb-noun>.use-case.ts
↓ (deps) => async (input: XInput) => XOutput
↓ ends with xOutputSchema.parse(result)
Repository implementation ← infrastructure/repositories/<noun>.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<typeof xUseCase> (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<ReturnType<typeof presenter>>. 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<void> 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
package.jsondeps — only declare allowed depsexportsmap — each feature exposes a small public surface (.,./ui,./cms,./api,./di/bind-production,./di/bind-dev-seed)- 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
- ESLint
Both use the same five-tag model; see "Five tags" section below.
Five tags
The workspace is organized into five mutually exclusive tags:
- app (3 packages):
apps/cms,apps/web-next,apps/storybook - core-composition (3 packages):
packages/core-api,packages/core-cms(must-have). Pluspackages/core-trpc(optional, currently scaffolded). - core (8 packages):
packages/core-shared(must-have). Plus the optional corescore-ui,core-realtime,core-events,core-audit,core-analytics,core-consent,core-dsr(all currently scaffolded; new ones viapnpm turbo gen core-package <name>). - feature (1 package):
packages/auth - 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/<feature> 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-apimay import from@repo/<feature>/apisubpath exports onlycore-cmsmay import from@repo/<feature>/cmssubpath exports onlycore-trpcreaches features transitively throughcore-api'sAppRoutertype
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— request and event flow through one feature, with the DI binding mode toggle and feature swap controls.di-explainer.html— the full container + symbols + binder lifecycle.feature-conformance-explainer.html— companion to ADR-020-adjacent conformance design (also linked fromagent-first-workflow-and-conformance.mdandrunbook.md).audit-and-compliance-explainer.html— companion to ADR-018 (also linked fromdependency-flow.mdandguides/audit-and-compliance.md).
The four pages cross-link to each other; entering any of them surfaces the others.