9.7 KiB
Website Architecture Contract (Strict)
This document is the authoritative contract for apps/website.
If any other website document conflicts with this one, this one wins.
1) Purpose (non-negotiable)
The website is a delivery layer.
It does not:
- contain business rules
- make authorization decisions
- own or persist business truth
It only:
- renders truth from
apps/api - collects user intent
- forwards user intent to
apps/api
The API is the single source of truth.
2) System context (hard boundary)
The website never bypasses the API.
Browser
↓
Next.js App Router (RSC + Server Actions)
↓
HTTP
↓
Backend API (Use Cases, Domain, Database)
3) Website presentation model types (strict)
3.1 API Transport DTO
Definition: the shape returned by the backend API over HTTP.
Rules:
- API Transport DTOs MUST be contained inside infrastructure.
- API Transport DTOs MUST NOT be imported by Templates.
Canonical placement in this repo:
apps/website/lib/types/**(transport DTOs consumed by services and page queries)
3.2 ViewData
Definition: the only allowed input type for Templates.
Rules:
- JSON-serializable only.
- Contains only template-ready values (mostly strings/numbers/booleans).
- MUST NOT contain class instances.
- Uncle Bob says: "Data structures should not have behavior."
See VIEW_DATA.md.
Canonical placement in this repo:
apps/website/templates/**(Templates that accept ViewData only)
3.3 ViewModel
Definition: the client-only, UI-owned class representing fully prepared UI state.
Rules:
- Instantiated only in
'use client'modules. - Never serialized.
- Used for client components that need state management.
- Uncle Bob says: "Objects expose behavior, not data."
See VIEW_MODELS.md.
Canonical placement in this repo:
apps/website/lib/view-models/**
4) Data transformation helpers (strict)
4.1 ViewModel Builder
Definition: transforms API Transport DTOs into ViewModels.
Purpose: prepare raw API data for client-side state management.
Rules:
- MUST be deterministic.
- MUST be side-effect free.
- MUST NOT call HTTP.
- MUST NOT call the API.
- Input:
Result<ApiDto, string>orApiDto - Output: ViewModel
- MUST use
static build()andsatisfies ViewModelBuilder.
See BUILDERS.md.
Canonical placement in this repo:
apps/website/lib/builders/view-models/**
4.2 ViewData Builder
Definition: transforms API DTOs directly into ViewData for templates.
Purpose: prepare API data for server-side rendering.
Rules:
- MUST be deterministic.
- MUST be side-effect free.
- MUST NOT call HTTP.
- MUST NOT call the API.
- Input:
Result<ApiDto, string>orApiDto - Output: ViewData
- MUST use
static build()andsatisfies ViewDataBuilder.
See BUILDERS.md.
Canonical placement in this repo:
apps/website/lib/builders/view-data/**
4.3 Result Type
Definition: Type-safe error handling for all operations.
Purpose: eliminate exceptions and provide explicit error paths.
Rules:
- All PageQueries return
Result<ViewData, PresentationError> - All Mutations return
Result<void, MutationError> - All Services return
Result<ApiDto, DomainError> - Use
Result.ok(value)for success - Use
Result.err(error)for errors - Never throw exceptions
See Result.ts.
Canonical placement in this repo:
apps/website/lib/contracts/Result.ts
4.4 Formatter & Display Object
Definition: deterministic, reusable, UI-only formatting/mapping logic.
Rules:
- Formatters: Stateless utilities for server-side primitive output.
- Display Objects: Rich Value Objects for client-side interactive APIs.
- MUST NOT call
Intl.*ortoLocale*(unless client-only). - MUST NOT implement business rules.
See FORMATTERS.md.
Canonical placement in this repo:
apps/website/lib/display-objects/**
5) Read flow (strict)
Server Components (RSC)
RSC page.tsx
↓
PageQuery (manual construction)
↓
Service (creates own API Client, Logger, ErrorReporter)
↓
API Client (makes HTTP calls)
↓
API Transport DTO
↓
Result<ApiDto, DomainError>
↓
PageQuery (maps DomainError → PresentationError)
↓
Result<ViewData, PresentationError>
↓
ViewData Builder (lib/builders/view-data/)
↓
ViewData
↓
Template
Key Points:
- PageQuery constructs Service
- Service creates its own dependencies
- Service returns Result<ApiDto, DomainError>
- PageQuery maps errors to presentation layer
- Builder transforms API DTO to ViewData
Client Components
Client Component
↓
API client (useEffect)
↓
API Transport DTO
↓
Result<ApiDto, string>
↓
ViewModel Builder (lib/builders/view-models/)
↓
ViewModel (lib/view-models/)
↓
Client State (useState)
↓
Template
6) Write flow (strict)
All writes MUST enter through Next.js Server Actions.
Forbidden:
- client components performing write HTTP requests
- client components calling API clients for mutations
Allowed:
- client submits intent (FormData, button action)
- server action performs UX validation
- server action calls a mutation (not services directly)
- mutation orchestrates services for writes
Server Actions must use Mutations:
// ❌ WRONG - Direct service usage
'use server';
import { AdminService } from '@/lib/services/admin/AdminService';
export async function updateUserStatus(userId: string, status: string) {
const service = new AdminService();
await service.updateUserStatus(userId, status); // ❌ Should use mutation
}
// ✅ CORRECT - Mutation usage
'use server';
import { UpdateUserStatusMutation } from '@/lib/mutations/UpdateUserStatusMutation';
import { revalidatePath } from 'next/cache';
export async function updateUserStatus(input: UpdateUserStatusInput) {
const mutation = new UpdateUserStatusMutation();
const result = await mutation.execute(input);
if (result.isErr()) {
console.error('updateUserStatus failed:', result.error);
return { success: false, error: result.error };
}
revalidatePath('/admin/users');
return { success: true };
}
Pattern:
- Server Action (thin wrapper) - handles framework concerns (revalidation, returns to client)
- Mutation (framework-agnostic) - constructs Service, calls service methods
- Service - constructs own dependencies (API Client, Logger), returns Result
- API Client (infrastructure) - makes HTTP requests, throws HTTP errors
- Result - type-safe error handling at every layer
Dependency Flow:
Server Action
↓ (constructs)
Mutation
↓ (constructs)
Service (creates API Client, Logger, ErrorReporter)
↓ (calls)
API Client
↓ (HTTP)
Backend API
Rationale:
- Mutations are framework-agnostic (can be tested without Next.js)
- Consistent pattern with PageQueries
- Type-safe error handling with Result
- Makes infrastructure explicit and testable
- Manual construction (no DI container issues)
See MUTATIONS.md and SERVICES.md.
7) Authorization (strict)
- The website may hide/disable UI for UX.
- The website MUST NOT enforce security.
- The API enforces authentication and authorization.
See docs/architecture/shared/BLOCKERS_AND_GUARDS.md and docs/architecture/website/BLOCKERS.md.
7.1) Client state (strict)
Client-side state is allowed only for UI concerns.
Allowed:
- selection
- open/closed dialogs
- transient form state
- optimistic flags and loading spinners
Forbidden:
- treating client state as business truth
- using client state as an authorization decision
- persisting client state as the source of truth
Hard rule:
- any truth returned by the API MUST overwrite client assumptions.
Canonical placement in this repo:
apps/website/lib/blockers/**for UX-only prevention helpersapps/website/lib/hooks/**for React-only utilitiesapps/website/lib/command-models/**for transient form models
See CLIENT_STATE.md.
8) DI contract (Inversify) (strict)
The DI system under apps/website/lib/di/index.ts is client-first.
Server execution is concurrent. Any shared singleton container can leak cross-request state.
Rules:
- Server
app/**/page.tsxMUST NOT access the container. - Page Queries SHOULD prefer manual wiring.
- Client modules MAY use DI via
ContainerProviderand hooks. ContainerManager.getContainer()is client-only.- Any server DI usage MUST be request-scoped (a fresh container per request).
Hard constraint:
- A singleton Inversify container MUST NOT be used to serve concurrent server requests.
See WEBSITE_DI_RULES.md.
9) Non-negotiable rules (final)
- The API is the brain.
- The website is a terminal.
- API Transport DTOs never reach Templates.
- Templates accept ViewData only.
- Page Queries do not format; they only compose.
- All operations return
Result<T, E>for type-safe error handling. - ViewData Builders transform API DTO → ViewData (RSC).
- ViewModel Builders transform API DTO → ViewModel (Client).
- Builders are pure and deterministic.
- Server Actions are the only write entry point.
- Server Actions must use Mutations (not Services directly).
- Mutations orchestrate Services for writes.
- Authorization always belongs to the API.