Wires the existing turbo gen feature generator into AGENTS.md (Adding a Feature section, Key Commands, Specification & Guides) and CLAUDE.md (Quick Start, Read First). Adds a fast-path callout at the top of the manual walkthrough in docs/guides/adding-a-feature.md pointing at scaffolding-a-feature.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.0 KiB
Clean Architecture Monorepo Template
Quick Start
pnpm install # Install all dependencies
pnpm dev # Start all dev servers
pnpm build # Build all packages
pnpm test # Run all tests
pnpm turbo boundaries # Validate workspace dependency graph
pnpm turbo gen feature # Scaffold a new feature package (see docs/guides/scaffolding-a-feature.md)
docker compose up -d # Start PostgreSQL
TDD
pnpm test --watch --filter @repo/<feature> # 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. Core packages (core-shared, core-cms, core-api, core-trpc, core-ui) provide foundation. 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
AGENTS.md— Package map, boundary rules, per-package conventionsdocs/architecture/overview.md— High-level architecture and package responsibilitiesdocs/architecture/vertical-feature-spec.md— Design spec with rationale and decision logdocs/guides/scaffolding-a-feature.md—turbo gen featurereference (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 Phase-1 scope doesn't cover)
Key Conventions
- Relative imports in
src/— Source files use relative paths (../repositories/...), not@/alias @/alias in tests — Test files (*.test.ts) use@/to import fromsrc/vitest.config.ts— Every package must defineresolve.alias: { "@": path.resolve(__dirname, "./src") }tsconfig.jsonrootDir — Set"rootDir": "."so TypeScript finds bothsrc/and test files- Lazar-conformant file layout — Entities live at
entities/models/<x>.ts; errors atentities/errors/<domain>.ts+entities/errors/common.ts; mock siblings use the.mock.tssuffix (<x>.repository.mock.ts); real repository impls drop thepayload-prefix (<x>.repository.ts); interface filenames are dot-separated (<x>.repository.interface.ts) - Factory-function use cases & controllers — Every use case and controller is
(deps) => async (input) => result; each exportsexport type I*UseCase = ReturnType<typeof xUseCase>(and the analogousI*Controller); one controller per use case (no multi-method controllers) - DI uses
.toDynamicValue()for factories —bind<IXUseCase>(SYMBOL).toDynamicValue((ctx) => xUseCase(ctx.container.get(...))); mocks remain the default binding - Tests inject mocks directly — Construct
MockXRepositoryand 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(az.ZodObjectwith.strict();z.object({}).strict()for void inputs) and, for non-void use cases,xOutputSchema. Types:XInput = z.infer<typeof xInputSchema>andXOutput. Use case body ends withxOutputSchema.parse(result)before returning (runtime guarantee against malformed repository data) - Controllers receive
unknown+ presenter — ControllerssafeParse(xInputSchema)from the use-case file and throwInputParseErroron failure. Non-void controllers define a top-levelfunction presenter(value: XOutput)and returnPromise<ReturnType<typeof presenter>>(identity is fine —return value); void controllers returnPromise<void>with no presenter - Feature-scoped tRPC error mapping — Each feature has
integrations/api/procedures.tsexportingxProcedure = t.procedure.use(defineErrorMiddleware([[Ctor, "TRPC_CODE"], ...]))from@repo/core-shared/trpc/define-error-middleware. Routers usexProcedure.input(xInputSchema)— schemas are imported from the use-case file, never redefined inline.core-sharednever 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/<feature>/ui, schemas/types from@repo/<feature> - 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'sbindAll()dispatcher inapps/web-next/src/server/bind-production.tspicks one by env:USE_DEV_SEED="true"→ dev seed;NODE_ENV="production"→ production; otherwise → dev seed (developer default sopnpm devboots without Payload). Dev seed lives insrc/__seeds__/dev.tsas a lazybuildDev<Entities>()function that uses the feature's existing factory - 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/— Two interfaces (ITracer,ILogger), three implementations (NoopTracer/NoopLogger,SentryTracer/SentryLogger, andRecordingTracer/RecordingLoggerfromcore-testing). Feature packages MUST NOT import@sentry/*directly (R40, eslint-enforced) - Spans + capture composed at DI bind time — Use cases + controllers wrapped via
withSpan(tracer, spanOpts, withCapture(logger, tags, factory(deps)))insidebind-production/bind-dev-seed.withSpanis outermost so an errored span's timing reflects the capture-and-rethrow. Repository methods are different — they callthis.tracer.startSpan(...)andthis.logger.captureException(...)inline per method because they own per-call attributes (R41, R42) - Capture at throw sites only, with double-report guard — Repos capture infra errors inline; use cases + controllers capture via
withCaptureat bind time;defineErrorMiddlewarenever captures (R43, R44). Each error gets a non-enumerable__sentryReportedflag the first time it's captured;withCapture,SentryLogger, andRecordingLoggerall bail if the flag is set, so a bubbled error surfaces exactly once with the inner-most layer's tags (helper atcore-shared/instrumentation/reported-flag.ts) - PII handling is non-negotiable —
sendDefaultPii: falseeverywhere (R31, CI grep gate); replay default-masks all text/inputs/media (R34, R35, allowlist starts empty);Sentry.setUser({ id })only — no email/username (R36);beforeSend+beforeSendTransactionscrubbers strip emails/passwords/tokens/cookies/auth/IPs (R32, R33) - Three apps, three Sentry projects —
WEB_NEXT_SENTRY_DSN,CMS_SENTRY_DSN,WEB_TANSTACK_SENTRY_DSN. Browser DSNs useNEXT_PUBLIC_(web-next) andVITE_(web-tanstack) prefixes - Instrumentation binding is orthogonal to repo binding —
bindAll()'s Rule 0 (DSN → Sentry vs Noop) is independent ofUSE_DEV_SEED/NODE_ENV. Runpnpm devwithWEB_NEXT_SENTRY_DSNset to test the integration locally
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 |