Clean Architecture + DDD applied in this monorepo: layers, dependency rule, and enforcement.
Clean Architecture ensures business rules are independent of frameworks, databases, and delivery mechanisms. Dependencies always point inward — outer layers know about inner layers, never the reverse.
Domain-Driven Design (DDD) ensures the code reflects the real-world domain it models. Entities protect their own invariants, Value Objects enforce correctness at the type level, and bounded contexts prevent accidental coupling.
Dependencies point only inward:
core ← application ← infra ← site / admin
Site / Admin (Next.js, React)
│
▼
Application (use cases, ports)
│
▼
Core (domain — entities, VOs, repositories)
▲
│
Infra (Prisma + Supabase, external services)
core never knows about application, infra, or web/api. application never knows about infra.
| Layer | Package | Responsibility |
|---|---|---|
| Domain | @repo/core |
Entities, Value Objects, invariants, repository interfaces. Zero framework dependencies. |
| Application | @repo/application |
Use cases, ports (interfaces), DTOs/view models. Orchestrates the domain. |
| Infrastructure | @repo/infra |
Repository implementations (Prisma + Supabase), external service adapters. |
| Interface | apps/site, apps/admin |
Server Components call use cases directly (SSG/ISR); 'use client' components receive data as props. |
| Forbidden | Reason |
|---|---|
prisma, @prisma/client |
Infrastructure concern |
next, next/* |
Presentation concern |
react, react-dom |
Presentation concern |
axios, node-fetch |
HTTP client — infra concern |
@repo/application, @repo/infra |
Would invert dependency direction |
Allowed: plain TypeScript, @repo/utils, uuid.
- Forbidden: React, Next.js, Prisma, HTTP libraries
- Allowed:
@repo/core, TypeScript interfaces (ports) - Use cases orchestrate domain objects — never access the database directly
- Implements the port interfaces defined in
core/application - Knows
coreandapplication, never the other way around
- Server Components may import
@repo/applicationand call use cases directly (SSG/ISR). The code runs only at build time and never reaches the browser. 'use client'components must not import@repo/applicationor@repo/infra— client code ships to the browser. They receive data as props from Server Components.- No auth vendor in the UI:
apps/sitemust not import@supabase/*,next-auth/*, or other IdP SDKs from pages, layouts, client components, ormiddleware.ts. Authentication is pluggable behindIAuthenticationGateway(see 11-IDENTITY). - Never import concrete repositories from presentation code.
- React components must contain no business logic.
packages/eslint-config/core.js enforces packages/core restrictions at lint time:
'no-restricted-imports': ['error', {
patterns: [
{ group: ['@prisma/*', 'prisma'], message: 'core cannot import Prisma' },
{ group: ['next/*', 'next'], message: 'core cannot import Next.js' },
{ group: ['react', 'react-dom'], message: 'core cannot import React' },
{ group: ['axios', 'node-fetch'], message: 'core cannot import HTTP clients' },
],
}]packages/core,packages/application, andpackages/infraare in place (Portfolio + Identity domain, Prisma repositories, use cases, DTOs).apps/siteis a fully static site (SSG vianext start). Server Components call use cases directly throughgetServerContainer(). No API routes exist in this app.- Future: a dedicated backend will expose the REST surface defined in 05-API-CONTRACTS. When that happens,
apps/siteclient components will consume it viafetch.
// packages/typescript-config/base.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"paths": {
"@repo/core/*": ["../../packages/core/src/*"],
"@repo/application/*": ["../../packages/application/src/*"],
"@repo/infra/*": ["../../packages/infra/src/*"],
"@repo/ui/*": ["../../packages/ui/src/*"]
}
}
}- 03-BOUNDED-CONTEXTS — DDD contexts, aggregates,
packages/corestructure - 04-APPLICATION-LAYER — Use cases and ports
- packages/core/decisions/ — Architectural Decision Records