Files
gridpilot.gg/docs/architecture/website/WEBSITE_CONTRACT.md
2026-01-24 01:22:43 +01:00

372 lines
9.7 KiB
Markdown

# 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.
```text
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`](docs/architecture/website/VIEW_DATA.md:1).
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`](docs/architecture/website/VIEW_MODELS.md:1).
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>` or `ApiDto`
- Output: ViewModel
- MUST use `static build()` and `satisfies ViewModelBuilder`.
See [`BUILDERS.md`](docs/architecture/website/BUILDERS.md:1).
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>` or `ApiDto`
- Output: ViewData
- MUST use `static build()` and `satisfies ViewDataBuilder`.
See [`BUILDERS.md`](docs/architecture/website/BUILDERS.md:1).
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`](apps/website/lib/contracts/Result.ts:1).
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.*` or `toLocale*` (unless client-only).
- MUST NOT implement business rules.
See [`FORMATTERS.md`](docs/architecture/website/FORMATTERS.md:1).
Canonical placement in this repo:
- `apps/website/lib/display-objects/**`
## 5) Read flow (strict)
### Server Components (RSC)
```text
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
```text
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:**
```typescript
// ❌ 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**:
1. **Server Action** (thin wrapper) - handles framework concerns (revalidation, returns to client)
2. **Mutation** (framework-agnostic) - constructs Service, calls service methods
3. **Service** - constructs own dependencies (API Client, Logger), returns Result
4. **API Client** (infrastructure) - makes HTTP requests, throws HTTP errors
5. **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`](docs/architecture/website/MUTATIONS.md:1) and [`SERVICES.md`](docs/architecture/website/SERVICES.md:1).
## 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`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1) and [`docs/architecture/website/BLOCKERS.md`](docs/architecture/website/BLOCKERS.md:1).
## 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 helpers
- `apps/website/lib/hooks/**` for React-only utilities
- `apps/website/lib/command-models/**` for transient form models
See [`CLIENT_STATE.md`](docs/architecture/website/CLIENT_STATE.md:1).
## 8) DI contract (Inversify) (strict)
The DI system under [`apps/website/lib/di/index.ts`](apps/website/lib/di/index.ts:1) is **client-first**.
Server execution is concurrent. Any shared singleton container can leak cross-request state.
Rules:
1. Server `app/**/page.tsx` MUST NOT access the container.
2. Page Queries SHOULD prefer manual wiring.
3. Client modules MAY use DI via [`ContainerProvider`](apps/website/lib/di/index.ts:11) and hooks.
4. [`ContainerManager.getContainer()`](apps/website/lib/di/container.ts:74) is **client-only**.
5. 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`](docs/architecture/website/WEBSITE_DI_RULES.md:1).
## 9) Non-negotiable rules (final)
1. The API is the brain.
2. The website is a terminal.
3. API Transport DTOs never reach Templates.
4. Templates accept ViewData only.
5. Page Queries do not format; they only compose.
6. All operations return `Result<T, E>` for type-safe error handling.
7. ViewData Builders transform API DTO → ViewData (RSC).
8. ViewModel Builders transform API DTO → ViewModel (Client).
9. Builders are pure and deterministic.
10. Server Actions are the only write entry point.
11. Server Actions must use Mutations (not Services directly).
12. Mutations orchestrate Services for writes.
13. Authorization always belongs to the API.