This commit is contained in:
2026-01-11 13:04:33 +01:00
parent 6f2ab9fc56
commit 971aa7288b
44 changed files with 2168 additions and 1240 deletions

View File

@@ -0,0 +1,154 @@
Blockers & Guards
This document defines clear, non-overlapping responsibilities for Blockers (frontend) and Guards (backend).
The goal is to prevent semantic drift, security confusion, and inconsistent implementations.
Core Principle
Guards enforce. Blockers prevent.
• Guards protect the system.
• Blockers protect the UX.
There are no exceptions to this rule.
Backend — Guards (NestJS)
Definition
A Guard is a backend mechanism that enforces access or execution rules.
If a Guard denies execution, the request does not reach the application logic.
In NestJS, Guards implement CanActivate.
Responsibilities
Guards MAY:
• block requests entirely
• return HTTP errors (401, 403, 429)
• enforce authentication and authorization
• enforce rate limits
• enforce feature availability
• protect against abuse and attacks
Guards MUST:
• be deterministic
• be authoritative
• be security-relevant
Restrictions
Guards MUST NOT:
• depend on frontend state
• contain UI logic
• attempt to improve UX
• assume the client behaved correctly
Common Backend Guards
• AuthGuard
• RolesGuard
• PermissionsGuard
• ThrottlerGuard (NestJS)
• RateLimitGuard
• CsrfGuard
• FeatureFlagGuard
Summary (Backend)
• Guards decide
• Guards enforce
• Guards secure the system
Frontend — Blockers
Definition
A Blocker is a frontend mechanism that prevents an action from being executed.
Blockers exist solely to improve UX and reduce unnecessary requests.
Blockers are not security mechanisms.
Responsibilities
Blockers MAY:
• prevent multiple submissions
• disable actions temporarily
• debounce or throttle interactions
• hide or disable UI elements
• prevent navigation under certain conditions
Blockers MUST:
• be reversible
• be local to the frontend
• be treated as best-effort helpers
Restrictions
Blockers MUST NOT:
• enforce security
• claim authorization
• block access permanently
• replace backend Guards
• make assumptions about backend state
Common Frontend Blockers
• SubmitBlocker
• AuthBlocker
• RoleBlocker
• ThrottleBlocker
• NavigationBlocker
• FeatureBlocker
Summary (Frontend)
• Blockers prevent execution
• Blockers improve UX
• Blockers reduce mistakes and load
Clear Separation
Aspect Blocker (Frontend) Guard (Backend)
Purpose Prevent execution Enforce rules
Security ❌ No ✅ Yes
Authority ❌ Best-effort ✅ Final
Reversible ✅ Yes ❌ No
Failure effect UI feedback HTTP error
Naming Rules (Hard)
• Frontend uses *Blocker
• Backend uses *Guard
• Never mix the terms
• Never implement Guards in the frontend
• Never implement Blockers in the backend
Final Rule
If it must be enforced, it is a Guard.
If it only prevents UX mistakes, it is a Blocker.

View File

@@ -0,0 +1,156 @@
Command Models
This document defines Command Models as a first-class concept in the frontend architecture.
Command Models are UX-only write models used to collect, validate, and prepare user input
before it is sent to the backend as a Command DTO.
Command Models are not View Models and not Domain Models.
Purpose
A Form Model answers the question:
“What does the UI need in order to safely submit user input?”
Command Models exist to:
• centralize form state
• reduce logic inside components
• provide consistent client-side validation
• build Command DTOs explicitly
Core Rules
Command Models:
• exist only in the frontend
• are write-only (never reused for reads)
• are created per form
• are discarded after submission
Command Models MUST NOT:
• contain business logic
• enforce domain rules
• reference View Models
• reference Domain Entities or Value Objects
• be sent to the API directly
Relationship to Other Models
API DTO (read) → ViewModel → UI
UI Input → FormModel → Command DTO → API
• View Models are read-only
• Command Models are write-only
• No model is reused across read/write boundaries
Typical Responsibilities
A Form Model MAY:
• store field values
• track dirty / touched state
• perform basic UX validation
• expose isValid, canSubmit
• build a Command DTO
A Form Model MUST NOT:
• decide if an action is allowed
• perform authorization checks
• validate cross-aggregate rules
Validation Guidelines
Client-side validation is UX validation, not business validation.
Allowed validation examples:
• required fields
• min / max length
• email format
• numeric ranges
Forbidden validation examples:
• “user is not allowed”
• “league already exists”
• “quota exceeded”
Server validation is the source of truth.
Example: Simple Form Model (with class-validator)
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
export class SignupFormModel {
@IsEmail()
email = '';
@IsNotEmpty()
@MinLength(8)
password = '';
isSubmitting = false;
reset(): void {
this.email = '';
this.password = '';
}
toCommand(): SignupCommandDto {
return {
email: this.email,
password: this.password,
};
}
}
Usage in UI Component
const form = useFormModel(SignupFormModel);
async function onSubmit() {
if (!form.isValid()) return;
form.isSubmitting = true;
await authService.signup(form.toCommand());
}
The component:
• binds inputs to the Form Model
• reacts to validation state
• never builds DTOs manually
Testing
Command Models SHOULD be tested when they contain:
• validation rules
• non-trivial state transitions
• command construction logic
Command Models do NOT need tests if they only hold fields without logic.
Summary
• Command Models are UX helpers for writes
• They protect components from complexity
• They never replace backend validation
• They never leak into read flows
Command Models help users.
Use Cases protect the system.

View File

@@ -0,0 +1,125 @@
# Display Objects
## Definition
A **Display Object** encapsulates **reusable, UI-only display logic**.
In this codebase, a Display Object is a **Frontend Value Object**:
- class-based
- immutable
- deterministic
- side-effect free
It answers the question:
> “How should this specific piece of information be shown?”
Display Objects are **not screen-specific**.
They exist to avoid duplicating presentation logic across View Models.
---
## Responsibilities
A Display Object MAY:
- format values (money, dates, durations)
- handle localization and language-specific rules
- map codes to labels
- encapsulate UI display conventions
- be reused across multiple View Models
In addition, a Display Object MAY:
- normalize presentation inputs (for example trimming/casing)
- expose multiple explicit display variants (for example `shortLabel`, `longLabel`)
A Display Object MUST:
- be deterministic
- be side-effect free
- operate only on presentation data
A Display Object MUST:
- be implemented as a **class** with a small, explicit API
- accept only primitives/plain data in its constructor (or static factory)
- expose only primitive outputs (strings/numbers/booleans)
---
## Restrictions
A Display Object MUST NOT:
- contain business logic
- enforce domain invariants
- perform validation
- influence system behavior
- be sent back to the server
- depend on backend or infrastructure concerns
In this repository, a Display Object MUST NOT:
- call `Intl.*`
- call `Date.toLocaleString()` / `Date.toLocaleDateString()` / `Date.toLocaleTimeString()`
Reason: these are runtime-locale/timezone dependent and cause SSR/hydration mismatches.
If a rule affects system correctness or persistence,
it does not belong in a Display Object.
---
## Ownership & Placement
- Display Objects belong to the **presentation layer**
- They are frontend-only
- They are not shared with the backend or core
Placement rule (strict):
- Display Objects live under `apps/website/lib/display-objects/*`.
---
## Relationship to View Models
- View Models MAY use Display Objects
- Display Objects MUST NOT depend on View Models
- Display Objects represent **parts**
- View Models represent **screens**
Additional strict rules:
- View Models SHOULD compose Display Objects.
- Display Objects MUST NOT be serialized or passed across boundaries.
- They must not appear in server-to-client DTOs.
- Templates should receive primitive display outputs, not Display Object instances.
---
## Testing
Display Objects SHOULD be tested because they often contain:
- locale-specific behavior
- formatting rules
- edge cases visible to users
Additionally:
- test determinism by running the same inputs under Node and browser contexts (where applicable)
- test boundary rules (no `Intl.*`, no `toLocale*`)
---
## Summary
- Display Objects encapsulate **how something looks**
- View Models encapsulate **what a screen needs**
- Both are presentation concerns
- Neither contains business truth
In one sentence: Display Objects are **Value Objects for UI display**, not utility functions.

View File

@@ -0,0 +1,156 @@
Form Submission Flow (UI → System)
This document defines the only valid data flow when a user submits a form.
It applies to all write operations (create, update, delete).
There are no exceptions.
Core Principle
Read and Write paths are different.
What is displayed is never sent back.
High-Level Flow
UI → Command DTO → API → Core Use Case → Persistence
• View Models are read-only
• Display Objects are read-only
• Commands are write-only
1. UI (Component)
Responsibility
• Collect user input
• Manage UX state (loading, disabled, local errors)
Rules
• Only primitives are handled (string, number, boolean)
• No DTO reuse
• No ViewModel reuse
• No domain objects
The UI does not decide whether an action is allowed.
2. Form Model (Optional)
Responsibility
• Local form state
• Client-side validation (required, min/max length)
• Field-level errors
Rules
• UX-only validation
• No business rules
• Never shared with API or Core
3. Command DTO (Frontend)
Responsibility
• Express intent
• Represent a write operation
Rules
• Created fresh on submit
• Never derived from a ViewModel
• Never reused from read DTOs
• Write-only
4. Frontend Service
Responsibility
• Orchestrate the write action
• Call the API Client
• Propagate success or failure
Rules
• No business logic
• No validation
• No UI decisions
• No ViewModel creation for writes (except explicit success summaries)
5. API Client (Frontend)
Responsibility
• Perform HTTP request
• Handle transport-level failures
Rules
• Stateless
• No retries unless explicitly designed
• Throws technical errors only
6. API Layer (Backend)
Responsibility
• HTTP boundary
• Transport validation (schema / class-validator)
• Map API DTO → Core Command
Rules
• No business logic
• No persistence
• No UI concerns
7. Core Use Case
Responsibility
• Enforce business rules
• Validate domain invariants
• Change system state
Rules
• Single source of truth
• No UI logic
• No HTTP knowledge
Response Handling
Success
• API returns a Result DTO (IDs, status)
• Frontend reacts by:
• navigation
• reload via GET
• toast / confirmation
Failure
• Business errors → user-visible message
• Technical errors → error boundary / monitoring
Forbidden Patterns
• ViewModel → Command
• DisplayObject → API
• DTO roundtrip
• Domain Object in UI
• Reusing read models for writes
Summary
• Read Flow: DTO → ViewModel → UI
• Write Flow: UI → Command DTO → Core
What is shown is never sent back.

View File

@@ -0,0 +1,132 @@
# Login Flow State Machine Architecture
## Problem
The current login page has unpredictable behavior due to:
- Multiple useEffect runs with different session states
- Race conditions between session loading and redirect logic
- Client-side redirects that interfere with test expectations
## Solution: State Machine Pattern
### State Definitions
```typescript
enum LoginState {
UNAUTHENTICATED = "UNAUTHENTICATED",
AUTHENTICATED_WITH_PERMISSIONS = "AUTHENTICATED_WITH_PERMISSIONS",
AUTHENTICATED_WITHOUT_PERMISSIONS = "AUTHENTICATED_WITHOUT_PERMISSIONS",
POST_AUTH_REDIRECT = "POST_AUTH_REDIRECT"
}
```
### State Transition Table
| Current State | Session | ReturnTo | Next State | Action |
|---------------|---------|----------|------------|--------|
| INITIAL | null | any | UNAUTHENTICATED | Show login form |
| INITIAL | exists | '/dashboard' | AUTHENTICATED_WITH_PERMISSIONS | Redirect to dashboard |
| INITIAL | exists | NOT '/dashboard' | AUTHENTICATED_WITHOUT_PERMISSIONS | Show permission error |
| UNAUTHENTICATED | exists | any | POST_AUTH_REDIRECT | Redirect to returnTo |
| AUTHENTICATED_WITHOUT_PERMISSIONS | exists | any | POST_AUTH_REDIRECT | Redirect to returnTo |
### Class-Based Controller
```typescript
class LoginFlowController {
// Immutable state
private readonly session: AuthSessionDTO | null;
private readonly returnTo: string;
// State machine
private state: LoginState;
constructor(session: AuthSessionDTO | null, returnTo: string) {
this.session = session;
this.returnTo = returnTo;
this.state = this.determineInitialState();
}
private determineInitialState(): LoginState {
if (!this.session) return LoginState.UNAUTHENTICATED;
if (this.returnTo === '/dashboard') return LoginState.AUTHENTICATED_WITH_PERMISSIONS;
return LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS;
}
// Pure function - no side effects
getState(): LoginState {
return this.state;
}
// Pure function - returns action, doesn't execute
getNextAction(): LoginAction {
switch (this.state) {
case LoginState.UNAUTHENTICATED:
return { type: 'SHOW_LOGIN_FORM' };
case LoginState.AUTHENTICATED_WITH_PERMISSIONS:
return { type: 'REDIRECT', path: '/dashboard' };
case LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS:
return { type: 'SHOW_PERMISSION_ERROR' };
case LoginState.POST_AUTH_REDIRECT:
return { type: 'REDIRECT', path: this.returnTo };
}
}
// Called after authentication
transitionToPostAuth(): void {
if (this.session) {
this.state = LoginState.POST_AUTH_REDIRECT;
}
}
}
```
### Benefits
1. **Predictable**: Same inputs always produce same outputs
2. **Testable**: Can test each state transition independently
3. **No Race Conditions**: State determined once at construction
4. **Clear Intent**: Each state has a single purpose
5. **Maintainable**: Easy to add new states or modify transitions
### Usage in Login Page
```typescript
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { session } = useAuth();
const returnTo = searchParams.get('returnTo') ?? '/dashboard';
// Create controller once
const controller = useMemo(() =>
new LoginFlowController(session, returnTo),
[session, returnTo]
);
// Get current state
const state = controller.getState();
const action = controller.getNextAction();
// Execute action (only once)
useEffect(() => {
if (action.type === 'REDIRECT') {
router.replace(action.path);
}
}, [action, router]);
// Render based on state
if (state === LoginState.UNAUTHENTICATED) {
return <LoginForm />;
}
if (state === LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS) {
return <PermissionError returnTo={returnTo} />;
}
// Show loading while redirecting
return <LoadingSpinner />;
}
```
This eliminates all the unpredictable behavior and makes the flow testable and maintainable.

View File

@@ -0,0 +1,46 @@
# ViewData (Website Templates)
ViewData is the **only** allowed input type for Templates in `apps/website`.
## 1) Definition
ViewData is a JSON-serializable, template-ready data structure:
- primitives (strings/numbers/booleans)
- arrays and plain objects
- `null` for missing values
## 2) What ViewData is NOT
ViewData is not:
- a Page DTO (raw transport)
- a ViewModel (client-only class)
- a Display Object instance
## 3) Construction rules
ViewData MUST be created in client code:
1) Initial SSR-safe render: `ViewData = fromDTO(PageDTO)`
2) Post-hydration render: `ViewData = fromViewModel(ViewModel)`
Templates MUST NOT compute derived values.
## 4) Determinism rules
Any formatting used to produce ViewData MUST be deterministic.
Forbidden anywhere in formatting code paths:
- `Intl.*`
- `Date.toLocaleString()` / `Date.toLocaleDateString()` / `Date.toLocaleTimeString()`
Reason: SSR and browser outputs can differ.
## 5) Relationship to Display Objects
Display Objects are used to implement formatting/mapping, but their instances MUST NOT be stored inside ViewData.
Only primitive outputs produced by Display Objects may appear in ViewData.

View File

@@ -0,0 +1,97 @@
# View Models
## Definition
A **View Model** represents a **fully prepared UI state**.
It answers the question:
> “What does the UI need in order to render this screen without thinking?”
View Models are **UI-owned** classes.
They do not represent business truth and do not enforce domain rules.
---
## Responsibilities
A View Model MAY:
- accept an API DTO as input
- derive UI-specific fields
- combine or reshape data for rendering
- perform formatting (dates, numbers, labels)
- handle localization and presentation logic
- use Display Objects for reusable UI concerns
In the website SSR/RSC architecture, View Models MAY compute view-only derived values, but MUST NOT be the type passed into Templates.
A View Model MUST:
- be fully usable by the UI without further computation
- expose only data and UI-oriented helpers
- be created in a consistent, explicit way
---
## Restrictions
A View Model MUST NOT:
- contain business logic
- validate domain rules
- enforce permissions or authorization
- contain domain entities or value objects
- perform side effects
- be sent back to the server
If a View Model decides whether something is *allowed* or *correct*,
that logic belongs in the Core, not here.
---
## Ownership & Placement
- View Models belong to the **frontend**
- They live close to the UI, not in shared or core layers
- They are not shared with the backend
---
## Creation Rules
This repository distinguishes **Page DTO**, **ViewModel**, and **ViewData**:
- Page DTO: server-to-client payload (JSON-serializable)
- ViewModel: client-only class (never serialized)
- ViewData: template input (JSON-serializable)
Rules (website):
1) View Models are created in client code only.
2) View Models are created from Page DTOs.
3) Templates MUST NOT accept View Models; Templates accept ViewData only.
4) View Models MUST compose Display Objects and produce ViewData (primitive outputs only).
Authoritative reference: [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1).
---
## Testing
View Models SHOULD be tested when they contain:
- formatting logic
- localization behavior
- non-trivial derived fields
View Models do NOT need tests if they only expose data without logic.
---
## Summary
- View Models describe **UI state**
- They are **presentation-focused**, not business-focused
- They reduce complexity in components
- They form a stable contract for the UI

View File

@@ -0,0 +1,30 @@
# Website DI Rules (Inversify)
This repo uses Inversify DI under [apps/website/lib/di](apps/website/lib/di/index.ts:1).
## 1) Non-negotiable safety rule
No stateful service instances may be shared across requests.
Reason: Next.js server execution is concurrent; shared state causes cross-request leakage.
## 2) Rules by module type
### 2.1 `page.tsx` (server)
- MUST NOT access the DI container directly.
- MUST call a PageQuery only.
### 2.2 Page Queries (server)
- SHOULD prefer explicit construction (manual wiring).
- MAY use DI only if all resolved services are stateless and safe for concurrent requests.
### 2.3 Client modules
- MAY use DI via `ContainerProvider` and hooks (example: `useInject`).
## 3) Container singleton warning
[`ContainerManager`](apps/website/lib/di/container.ts:61) holds a singleton container. Treat it as **unsafe for server request scope** unless proven otherwise.

View File

@@ -0,0 +1,30 @@
# Website Guardrails (Mandatory)
This document defines architecture guardrails that must be enforced via tests + ESLint.
## 1) RSC boundary guardrails
Fail CI if any `apps/website/app/**/page.tsx`:
- imports from `apps/website/lib/view-models/*`
- calls `Intl.*` or `toLocale*`
- performs sorting/filtering (`sort`, `filter`, `reduce`) beyond trivial null checks
## 2) Template purity guardrails
Fail CI if any `apps/website/templates/**`:
- imports from `apps/website/lib/view-models/*`
- imports from `apps/website/lib/display-objects/*`
- calls `Intl.*` or `toLocale*`
Templates accept ViewData only.
## 3) Display Object guardrails
Fail CI if any `apps/website/lib/display-objects/**`:
- calls `Intl.*` or `toLocale*`
Display Objects must be deterministic.

View File

@@ -0,0 +1,39 @@
# Website Page Queries (Server)
This document defines the only allowed server-side data fetching shape for `apps/website` routes.
## 1) Purpose
Page Queries are server-side composition classes that:
- call services that call `apps/api`
- assemble a Page DTO
- return an explicit result describing route outcome
They do not implement business rules.
## 2) Result type (no null)
Page Queries MUST return a discriminated union (`PageQueryResult`):
- `ok` with `{ dto }`
- `notFound`
- `redirect` with `{ to }`
- `error` with `{ errorId }`
Pages MUST switch on this result and call:
- `notFound()` for `notFound`
- `redirect()` for `redirect`
## 3) Forbidden responsibilities
Page Queries MUST NOT:
- format values for display
- sort/filter (canonical or view-only)
- instantiate ViewModels
- instantiate Display Objects
If sorting/filtering is needed, it MUST be added to `apps/api`.

View File

@@ -0,0 +1,60 @@
# Website RSC Presentation Architecture (Strict)
This document defines the only allowed presentation architecture for `apps/website` (Next.js App Router).
It is **website-only** and does not change `apps/api` or `core` architecture.
## 1) Core rule: API owns business truth
- `apps/api` is the only source of truth for business rules and canonical filtering/sorting.
- `apps/website` is presentation infrastructure: composition, routing, caching, and rendering.
## 2) The three website presentation data types
### 2.1 Page DTO
**Purpose:** server-to-client payload.
**Rules:**
- JSON-serializable only.
- Contains raw values only (ISO date strings, numbers, codes).
- MUST NOT contain class instances.
### 2.2 ViewModel
**Purpose:** client-only presentation model.
**Rules:**
- Class-based.
- Instantiated only in `'use client'` modules.
- Composes Display Objects.
- NEVER passed into Templates.
### 2.3 ViewData
**Purpose:** Template input.
**Rules:**
- JSON-serializable only.
- Contains only values ready to render (mostly strings/numbers).
- Built from Page DTO (initial render) and from ViewModel (post-hydration).
## 3) Required per-route structure
Every route MUST follow:
1) `page.tsx` (server): calls a PageQuery and passes Page DTO
2) `*PageClient.tsx` (client): builds ViewData and renders Template
3) `*Template.tsx` (pure UI): renders ViewData only
## 4) Authoritative specification
This document is an entry point only.
The authoritative, test-enforced spec lives at:
- [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1)