This commit is contained in:
2026-01-11 14:42:54 +01:00
parent 2f0b83f030
commit 90b6e73a22
27 changed files with 980 additions and 2513 deletions

View File

@@ -0,0 +1,78 @@
# API Data Flow (Strict)
This document defines the **apps/api** data flow and responsibilities.
API scope:
- `apps/api/**`
## 1) API role
The API is a **delivery application**.
Responsibilities:
- HTTP transport boundary
- authentication and authorization enforcement
- request validation (transport shape)
- mapping between HTTP DTOs and Core inputs
- calling Core use cases
- mapping Core results into HTTP responses
## 2) API data types (strict)
### 2.1 Request DTO
Definition: HTTP request contract shape.
Rules:
- lives in the API layer
- validated at the API boundary
- never enters Core unchanged
### 2.2 Response DTO
Definition: HTTP response contract shape.
Rules:
- lives in the API layer
- never contains domain objects
### 2.3 API Presenter
Definition: mapping logic from Core results to HTTP response DTOs.
Rules:
- pure transformation
- no business rules
- may hold state per request
## 3) Canonical flow
```text
HTTP Request
Guards (auth, authorization, feature availability)
Controller (transport-only)
Mapping: Request DTO → Core input
Core Use Case
Mapping: Core result → Response DTO (Presenter)
HTTP Response
```
## 4) Non-negotiable rules
1. Controllers contain no business rules.
2. Controllers do not construct domain objects.
3. Core results never leave the API without mapping.
See authorization model: [`docs/architecture/api/AUTHORIZATION.md`](docs/architecture/api/AUTHORIZATION.md:1).

View File

@@ -0,0 +1,38 @@
# API File Structure (Strict)
This document defines the canonical **physical** structure for `apps/api/**`.
It describes where code lives, not the full behavioral rules.
## 1) API is feature-based
The API is organized by feature modules.
```text
apps/api/
src/
domain/
shared/
```
Within feature modules:
```text
apps/api/src/domain/<feature>/
<Feature>Controller.ts
<Feature>Service.ts
<Feature>Module.ts
dto/
presenters/
```
## 2) What belongs where (strict)
- Controllers: HTTP boundary only
- DTOs: HTTP contracts only
- Presenters: map Core results  response DTOs
API flow rules:
- [`docs/architecture/api/API_DATA_FLOW.md`](docs/architecture/api/API_DATA_FLOW.md:1)

View File

@@ -12,7 +12,7 @@ It complements (but does not replace) feature availability:
- Authorization answers: “Is this actor allowed to do it?”
Related:
- Feature gating concept: docs/architecture/FEATURE_AVAILABILITY.md
- Feature gating concept: [`docs/architecture/shared/FEATURE_AVAILABILITY.md`](docs/architecture/shared/FEATURE_AVAILABILITY.md:1)
---
@@ -154,7 +154,7 @@ Return **404** when:
Use this sparingly and intentionally.
### 6.3 Feature availability interaction
Feature availability failures (disabled/hidden/coming soon) should behave as “not found” for public callers, while maintenance mode should return 503. See docs/architecture/FEATURE_AVAILABILITY.md.
Feature availability failures (disabled/hidden/coming soon) should behave as “not found” for public callers, while maintenance mode should return 503. See [`docs/architecture/shared/FEATURE_AVAILABILITY.md`](docs/architecture/shared/FEATURE_AVAILABILITY.md:1).
---
@@ -253,4 +253,4 @@ Rules:
- A super-admin UI can manage:
- global roles (owner/admin)
- scoped roles (league_owner/admin/steward, sponsor_owner/admin, team_owner/admin)
- Feature availability remains a separate control plane (maintenance mode, coming soon, kill switches), documented in docs/architecture/FEATURE_AVAILABILITY.md.
- Feature availability remains a separate control plane (maintenance mode, coming soon, kill switches), documented in [`docs/architecture/shared/FEATURE_AVAILABILITY.md`](docs/architecture/shared/FEATURE_AVAILABILITY.md:1).

View File

@@ -0,0 +1,47 @@
# Authentication and Authorization Flow (API)
This document defines how authentication and authorization are enforced in the API.
Shared contract:
- [`docs/architecture/shared/AUTH_CONTRACT.md`](docs/architecture/shared/AUTH_CONTRACT.md:1)
## 1) Enforcement location (strict)
All enforcement happens in the API.
The API must:
- authenticate the actor from the session
- authorize the actor for the requested capability
- deny requests deterministically with appropriate HTTP status
## 2) Canonical request flow
```text
HTTP Request
Authentication (resolve actor)
Authorization (roles, permissions, scope)
Controller (transport-only)
Core Use Case
Presenter mapping
HTTP Response
```
## 3) Non-negotiable rules
1. Deny by default unless explicitly public.
2. The actor identity is derived from the session.
3. Controllers do not contain business rules.
Related:
- Authorization model: [`docs/architecture/api/AUTHORIZATION.md`](docs/architecture/api/AUTHORIZATION.md:1)
- Guards definition: [`docs/architecture/api/GUARDS.md`](docs/architecture/api/GUARDS.md:1)

View File

@@ -0,0 +1,47 @@
# Guards (API Enforcement)
This document defines **Guards** as API enforcement mechanisms.
Shared contract: [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1)
## 1) Definition
A Guard is an API mechanism that enforces access or execution rules.
If a Guard denies execution, the request does not reach application logic.
## 2) 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
## 3) Restrictions
Guards MUST NOT:
- depend on website/client state
- contain UI logic
- attempt to improve UX
- assume the client behaved correctly
## 4) Common Guards
- AuthGuard
- RolesGuard
- PermissionsGuard
- Throttler/RateLimit guards
- CSRF guards
- Feature availability guards

View File

@@ -0,0 +1,38 @@
# Use Case Wiring (API) (Strict)
This document defines how the API wires HTTP requests to Core Use Cases.
Core contract:
- [`docs/architecture/core/USECASES.md`](docs/architecture/core/USECASES.md:1)
## 1) Non-negotiable rules
1. Controllers are transport boundaries.
2. Controllers validate request DTOs.
3. Controllers map request DTOs to Core inputs.
4. Controllers execute Core Use Cases.
5. Controllers map Core results to response DTOs.
## 2) Presenter meaning in the API
In the API, a Presenter is an output adapter that maps Core results to HTTP response DTOs.
Rule:
- API presenters are request-scoped. They must not be shared across concurrent requests.
## 3) Canonical flow
```text
HTTP Request DTO
Controller mapping
Core Use Case
Presenter mapping
HTTP Response DTO
```

View File

@@ -0,0 +1,92 @@
# Core Data Flow (Strict)
This document defines the **Core** data flow rules and boundaries.
Core scope:
- `core/**`
Core does not know:
- HTTP
- Next.js
- databases
- DTOs
- UI models
## 1) Layers inside Core
Core contains two inner layers:
- Domain
- Application
### 1.1 Domain
Domain is business truth.
Allowed:
- Entities
- Value Objects
- Domain Services
Forbidden:
- DTOs
- frameworks
- IO
See [`docs/architecture/core/DOMAIN_OBJECTS.md`](docs/architecture/core/DOMAIN_OBJECTS.md:1).
### 1.2 Application
Application coordinates business intents.
Allowed:
- Use Cases (commands and queries)
- Application-level ports (repository ports, gateways)
Forbidden:
- HTTP
- persistence implementations
- frontend models
## 2) Core I/O boundary
All communication across the Core boundary occurs through **ports**.
Rules:
- Port interfaces live in Core.
- Implementations live outside Core.
## 3) Core data types (strict)
- Use Case inputs are plain data and/or domain types.
- Use Case outputs are plain data and/or domain types.
Core MUST NOT define HTTP DTOs.
## 4) Canonical flow
```text
Delivery App (HTTP or Website)
Core Application (Use Case)
Core Domain (Entities, Value Objects)
Ports (repository, gateway)
Adapter implementation (outside Core)
```
## 5) Non-negotiable rules
1. Core is framework-agnostic.
2. DTOs do not enter Core.
3. Core defines ports; outer layers implement them.

View File

@@ -0,0 +1,57 @@
# Core File Structure (Strict)
This document defines the canonical **physical** structure for `core/`.
It describes where code lives, not the full behavioral rules.
Core rules and responsibilities are defined elsewhere.
## 1) Core is feature-based
Core is organized by bounded context / feature.
```text
core/
shared/
<context>/
domain/
application/
```
## 2) `core/<context>/domain/`
Domain contains business truth.
Canonical folders:
```text
core/<context>/domain/
entities/
value-objects/
services/
events/
errors/
```
See [`docs/architecture/core/DOMAIN_OBJECTS.md`](docs/architecture/core/DOMAIN_OBJECTS.md:1).
## 3) `core/<context>/application/`
Application coordinates business intents.
Canonical folders:
```text
core/<context>/application/
commands/
queries/
use-cases/
services/
ports/
```
See:
- [`docs/architecture/core/USECASES.md`](docs/architecture/core/USECASES.md:1)
- [`docs/architecture/core/CQRS.md`](docs/architecture/core/CQRS.md:1)

View File

@@ -196,15 +196,16 @@ Avoid CQRS Light when:
12. Migration Path
12. Adoption Rule (Strict)
CQRS Light allows incremental adoption:
1. Start with classic Clean Architecture
2. Separate commands and queries logically
3. Optimize read paths as needed
4. Introduce events or projections later (optional)
CQRS Light is a structural rule inside Core.
No rewrites required.
If CQRS Light is used:
- commands and queries MUST be separated by responsibility
- queries MUST remain read-only and must not enforce invariants
This document does not define a migration plan.
@@ -241,4 +242,4 @@ Without:
• Event sourcing complexity
• Premature optimization
It is the safest way to gain CQRS benefits while staying true to Clean Architecture.
It is the safest way to gain CQRS benefits while staying true to Clean Architecture.

View File

@@ -1,339 +0,0 @@
Enums in Clean Architecture (Strict & Final)
This document defines how enums are modeled, placed, and used in a strict Clean Architecture setup.
Enums are one of the most common sources of architectural leakage. This guide removes all ambiguity.
1. Core Principle
Enums represent knowledge.
Knowledge must live where it is true.
Therefore:
• Not every enum is a domain enum
• Enums must never cross architectural boundaries blindly
• Ports must remain neutral
2. Enum Categories (Authoritative)
There are four and only four valid enum categories:
1. Domain Enums
2. Application (Workflow) Enums
3. Transport Enums (API)
4. UI Enums (Frontend)
Each category has strict placement and usage rules.
3. Domain Enums
Definition
A Domain Enum represents a business concept that:
• has meaning in the domain
• affects rules or invariants
• is part of the ubiquitous language
Examples:
• LeagueVisibility
• MembershipRole
• RaceStatus
• SponsorshipTier
• PenaltyType
Placement
core/<context>/domain/
├── value-objects/
│ └── LeagueVisibility.ts
└── entities/
Preferred: model domain enums as Value Objects instead of enum keywords.
Example (Value Object)
export class LeagueVisibility {
private constructor(private readonly value: 'public' | 'private') {}
static from(value: string): LeagueVisibility {
if (value !== 'public' && value !== 'private') {
throw new DomainError('Invalid LeagueVisibility');
}
return new LeagueVisibility(value);
}
isPublic(): boolean {
return this.value === 'public';
}
}
Usage Rules
Allowed:
• Domain
• Use Cases
Forbidden:
• Ports
• Adapters
• API DTOs
• Frontend
Domain enums must never cross a Port boundary.
4. Application Enums (Workflow Enums)
Definition
Application Enums represent internal workflow or state coordination.
They are not business truth and must not leak.
Examples:
• LeagueSetupStep
• ImportPhase
• ProcessingState
Placement
core/<context>/application/internal/
└── LeagueSetupStep.ts
Example
export enum LeagueSetupStep {
CreateLeague,
CreateSeason,
AssignOwner,
Notify
}
Usage Rules
Allowed:
• Application Services
• Use Cases
Forbidden:
• Domain
• Ports
• Adapters
• Frontend
These enums must remain strictly internal.
5. Transport Enums (API DTOs)
Definition
Transport Enums describe allowed values in HTTP contracts.
They exist purely to constrain transport data, not to encode business rules.
Naming rule:
Transport enums MUST end with Enum.
This makes enums immediately recognizable in code reviews and prevents silent leakage.
Examples:
• LeagueVisibilityEnum
• SponsorshipStatusEnum
• PenaltyTypeEnum
Placement
apps/api/<feature>/dto/
└── LeagueVisibilityEnum.ts
Website mirrors the same naming:
apps/website/lib/dtos/
└── LeagueVisibilityEnum.ts
Example
export enum LeagueVisibilityEnum {
Public = 'public',
Private = 'private'
}
Usage Rules
Allowed:
• API Controllers
• API Presenters
• Website API DTOs
Forbidden:
• Core Domain
• Use Cases
Transport enums are copies, never reexports of domain enums.
Placement
apps/api/<feature>/dto/
└── LeagueVisibilityDto.ts
or inline as union types in DTOs.
Example
export type LeagueVisibilityDto = 'public' | 'private';
Usage Rules
Allowed:
• API Controllers
• API Presenters
• Website API DTOs
Forbidden:
• Core Domain
• Use Cases
Transport enums are copies, never reexports of domain enums.
6. UI Enums (Frontend)
Definition
UI Enums describe presentation or interaction state.
They have no business meaning.
Examples:
• WizardStep
• SortOrder
• ViewMode
• TabKey
Placement
apps/website/lib/ui/
└── LeagueWizardStep.ts
Example
export enum LeagueWizardStep {
Basics,
Structure,
Scoring,
Review
}
Usage Rules
Allowed:
• Frontend only
Forbidden:
• Core
• API
7. Absolute Prohibitions
❌ Enums in Ports
// ❌ forbidden
export interface CreateLeagueInputPort {
visibility: LeagueVisibility;
}
✅ Correct
export interface CreateLeagueInputPort {
visibility: 'public' | 'private';
}
Mapping happens inside the Use Case:
const visibility = LeagueVisibility.from(input.visibility);
8. Decision Checklist
Ask these questions:
1. Does changing this enum change business rules?
• Yes → Domain Enum
• No → continue
2. Is it only needed for internal workflow coordination?
• Yes → Application Enum
• No → continue
3. Is it part of an HTTP contract?
• Yes → Transport Enum
• No → continue
4. Is it purely for UI state?
• Yes → UI Enum
9. Summary Table
Enum Type Location May Cross Ports Scope
Domain Enum core/domain ❌ No Business rules
Application Enum core/application ❌ No Workflow only
Transport Enum apps/api + website ❌ No HTTP contracts
UI Enum apps/website ❌ No Presentation only
10. Final Rule (Non-Negotiable)
If an enum crosses a boundary, it is in the wrong place.
This rule alone prevents most long-term architectural decay.

View File

@@ -1,513 +1,62 @@
Use Case Architecture Guide
# Use Cases (Core Application Boundary) (Strict)
This document defines the correct structure and responsibilities of Application Use Cases
according to Clean Architecture, in a NestJS-based system.
This document defines the strict rules for Core Use Cases.
The goal is:
• strict separation of concerns
• correct terminology (no fake "ports")
• minimal abstractions
• long-term consistency
Scope:
This is the canonical reference for all use cases in this codebase.
- `core/**`
Non-scope:
1. Core Concepts (Authoritative Definitions)
- HTTP controllers
- DTOs
- Next.js pages
Use Case
• Encapsulates application-level business logic
• Is the Input Port
• Is injected via DI
• Knows no API, no DTOs, no transport
• Coordinates domain objects and infrastructure
## 1) Definition
The public execute() method is the input port.
A Use Case represents one business intent.
It answers:
Input
• Pure data
• Not a port
• Not an interface
• May be omitted if the use case has no parameters
- what the system does
type GetSponsorsInput = {
leagueId: LeagueId
}
## 2) Non-negotiable rules
1. Use Cases contain business logic.
2. Use Cases enforce invariants.
3. Use Cases do not know about HTTP.
4. Use Cases do not know about UI.
5. Use Cases do not depend on delivery-layer presenters.
6. Use Cases do not accept or return HTTP DTOs.
## 3) Inputs and outputs
Result
• The business outcome of a use case
• May contain Entities and Value Objects
• Not a DTO
• Never leaves the core directly
Inputs:
type GetSponsorsResult = {
sponsors: Sponsor[]
}
- plain data and/or domain types
Outputs:
- a `Result` containing plain data and/or domain types
Output Port
• A behavioral boundary
• Defines how the core communicates outward
• Never a data structure
• Lives in the Application Layer
Rule:
export interface UseCaseOutputPort<T> {
present(data: T): void
}
- mapping to and from HTTP DTOs happens in the API, not in the Core.
See API wiring: [`docs/architecture/api/USE_CASE_WIRING.md`](docs/architecture/api/USE_CASE_WIRING.md:1)
## 4) Ports
Presenter
• Implements UseCaseOutputPort<T>
• Lives in the API / UI layer
• Translates Result → ViewModel / DTO
• Holds internal state
• Is pulled by the controller after execution
2. Canonical Use Case Structure
Application Layer
Use Case
@Injectable()
export class GetSponsorsUseCase {
constructor(
private readonly sponsorRepository: ISponsorRepository,
private readonly output: UseCaseOutputPort<GetSponsorsResult>,
) {}
async execute(): Promise<Result<void, ApplicationError>> {
const sponsors = await this.sponsorRepository.findAll()
this.output.present({ sponsors })
return Result.ok(undefined)
}
}
Use Cases depend on ports for IO.
Rules:
• execute() is the Input Port
• The use case does not return result data
• All output flows through the OutputPort
• The return value signals success or failure only
### ⚠️ ARCHITECTURAL VIOLATION ALERT
- port interfaces live in Core
- implementations live in adapters or delivery apps
**The pattern shown above is INCORRECT and violates Clean Architecture.**
## 5) CQRS
#### ❌ WRONG PATTERN (What NOT to do)
If CQRS-light is used, commands and queries are separated by responsibility.
```typescript
@Injectable()
export class GetSponsorsUseCase {
constructor(
private readonly sponsorRepository: ISponsorRepository,
private readonly output: UseCaseOutputPort<GetSponsorsResult>,
) {}
See [`docs/architecture/core/CQRS.md`](docs/architecture/core/CQRS.md:1).
async execute(): Promise<Result<void, ApplicationError>> {
const sponsors = await this.sponsorRepository.findAll()
this.output.present({ sponsors }) // ❌ WRONG: Use case calling presenter
return Result.ok(undefined)
}
}
```
**Why this violates Clean Architecture:**
- Use cases **know about presenters** and how to call them
- Creates **tight coupling** between application logic and presentation
- Makes use cases **untestable** without mocking presenters
- Violates the **Dependency Rule** (inner layer depending on outer layer behavior)
#### ✅ CORRECT PATTERN (Clean Architecture)
```typescript
@Injectable()
export class GetSponsorsUseCase {
constructor(
private readonly sponsorRepository: ISponsorRepository,
// NO output port needed in constructor
) {}
async execute(): Promise<Result<GetSponsorsResult, ApplicationError>> {
const sponsors = await this.sponsorRepository.findAll()
return Result.ok({ sponsors })
// ✅ Returns Result, period. No .present() call.
}
}
```
**The Controller (in API layer) handles the wiring:**
```typescript
@Controller('/sponsors')
export class SponsorsController {
constructor(
private readonly useCase: GetSponsorsUseCase,
private readonly presenter: GetSponsorsPresenter,
) {}
@Get()
async getSponsors() {
// 1. Execute use case
const result = await this.useCase.execute()
if (result.isErr()) {
throw mapApplicationError(result.unwrapErr())
}
// 2. Wire to presenter
this.presenter.present(result.value)
// 3. Return ViewModel
return this.presenter.getViewModel()
}
}
```
**This is the ONLY pattern that respects Clean Architecture.**
Result Model
type GetSponsorsResult = {
sponsors: Sponsor[]
}
Rules:
• Domain objects are allowed
• No DTOs
• No interfaces
• No transport concerns
3. API Layer
API Services / Controllers (Thin Orchestration)
The API layer is a transport boundary. It MUST delegate business logic to `./core`:
• orchestrate auth + authorization checks (actor/session/roles)
• collect/validate transport input (DTOs at the boundary)
• execute a Core use case (entities/value objects live here)
• map Result → DTO / ViewModel via a Presenter (presenter owns mapping)
Rules:
• Controllers stay thin: no business rules, no domain validation, no decision-making
• API services orchestrate: auth + use case execution + presenter mapping
• Domain objects never cross the API boundary un-mapped
Presenter
@Injectable()
export class GetSponsorsPresenter
implements UseCaseOutputPort<GetSponsorsResult>
{
private viewModel!: GetSponsorsViewModel
present(result: GetSponsorsResult): void {
this.viewModel = {
sponsors: result.sponsors.map(s => ({
id: s.id.value,
name: s.name,
websiteUrl: s.websiteUrl,
})),
}
}
getViewModel(): GetSponsorsViewModel {
return this.viewModel
}
}
Controller
@Controller('/sponsors')
export class SponsorsController {
constructor(
private readonly useCase: GetSponsorsUseCase,
private readonly presenter: GetSponsorsPresenter,
) {}
@Get()
async getSponsors() {
const result = await this.useCase.execute()
if (result.isErr()) {
throw mapApplicationError(result.unwrapErr())
}
return this.presenter.getViewModel()
}
}
Payments Example
Application Layer
Use Case
@Injectable()
export class CreatePaymentUseCase {
constructor(
private readonly paymentRepository: IPaymentRepository,
private readonly output: UseCaseOutputPort<CreatePaymentResult>,
) {}
async execute(input: CreatePaymentInput): Promise<Result<void, ApplicationErrorCode<CreatePaymentErrorCode>>> {
// business logic
const payment = await this.paymentRepository.create(payment);
this.output.present({ payment });
return Result.ok(undefined);
}
}
Result Model
type CreatePaymentResult = {
payment: Payment;
};
API Layer
Presenter
@Injectable()
export class CreatePaymentPresenter
implements UseCaseOutputPort<CreatePaymentResult>
{
private viewModel: CreatePaymentViewModel | null = null;
present(result: CreatePaymentResult): void {
this.viewModel = {
payment: this.mapPaymentToDto(result.payment),
};
}
getViewModel(): CreatePaymentViewModel | null {
return this.viewModel;
}
reset(): void {
this.viewModel = null;
}
private mapPaymentToDto(payment: Payment): PaymentDto {
return {
id: payment.id,
// ... other fields
};
}
}
Controller
@Controller('/payments')
export class PaymentsController {
constructor(
private readonly useCase: CreatePaymentUseCase,
private readonly presenter: CreatePaymentPresenter,
) {}
@Post()
async createPayment(@Body() input: CreatePaymentInput) {
const result = await this.useCase.execute(input);
if (result.isErr()) {
throw mapApplicationError(result.unwrapErr());
}
return this.presenter.getViewModel();
}
}
4. Module Wiring (Composition Root)
@Module({
providers: [
GetSponsorsUseCase,
GetSponsorsPresenter,
{
provide: USE_CASE_OUTPUT_PORT,
useExisting: GetSponsorsPresenter,
},
],
})
export class SponsorsModule {}
Rules:
• The use case depends only on the OutputPort interface
• The presenter is bound as the OutputPort implementation
• process.env is not used inside the use case
5. Explicitly Forbidden
❌ DTOs in use cases
❌ Domain objects returned directly to the API
❌ Output ports used as data structures
❌ present() returning a value
❌ Input data named InputPort
❌ Mapping logic inside use cases
❌ Environment access inside the core
Do / Dont (Boundary Examples)
✅ DO: Keep pages/components consuming ViewModels returned by website services (DTOs stop at the service boundary), e.g. [LeagueAdminSchedulePage()](apps/website/app/leagues/[id]/schedule/admin/page.tsx:12).
✅ DO: Keep controllers/services thin and delegating, e.g. [LeagueController.createLeagueSeasonScheduleRace()](apps/api/src/domain/league/LeagueController.ts:291).
❌ DONT: Put business rules in the API layer; rules belong in `./core` use cases/entities/value objects, e.g. [CreateLeagueSeasonScheduleRaceUseCase.execute()](core/racing/application/use-cases/CreateLeagueSeasonScheduleRaceUseCase.ts:38).
6. Optional Extensions
Custom Output Ports
Only introduce a dedicated OutputPort interface if:
• multiple presentation paths exist
• streaming or progress updates are required
• more than one output method is needed
interface ComplexOutputPort {
presentSuccess(...)
presentFailure(...)
}
Input Port Interfaces
Only introduce an explicit InputPort interface if:
• multiple implementations of the same use case exist
• feature flags or A/B variants are required
• the use case itself must be substituted
Otherwise:
The use case class itself is the input port.
7. Key Rules (Memorize These)
Use cases answer what.
Presenters answer how.
Ports have behavior.
Data does not.
The core produces truth.
The API interprets it.
TL;DR
• Use cases are injected via DI
• execute() is the Input Port
• Outputs flow only through Output Ports
• Results are business models, not DTOs
• Interfaces exist only for behavior variability
### 🚨 CRITICAL CLEAN ARCHITECTURE CORRECTION
**The examples in this document (sections 2, 3, and the Payments Example) demonstrate the WRONG pattern that violates Clean Architecture.**
#### The Fundamental Problem
The current architecture shows use cases **calling presenters directly**:
```typescript
// ❌ WRONG - This violates Clean Architecture
this.output.present({ sponsors })
```
**This is architecturally incorrect.** Use cases must **never** know about presenters or call `.present()`.
#### The Correct Clean Architecture Pattern
**Use cases return Results. Controllers wire them to presenters.**
```typescript
// ✅ CORRECT - Use case returns data
@Injectable()
export class GetSponsorsUseCase {
constructor(private readonly sponsorRepository: ISponsorRepository) {}
async execute(): Promise<Result<GetSponsorsResult, ApplicationError>> {
const sponsors = await this.sponsorRepository.findAll()
return Result.ok({ sponsors })
// NO .present() call!
}
}
// ✅ CORRECT - Controller handles wiring
@Controller('/sponsors')
export class SponsorsController {
constructor(
private readonly useCase: GetSponsorsUseCase,
private readonly presenter: GetSponsorsPresenter,
) {}
@Get()
async getSponsors() {
const result = await this.useCase.execute()
if (result.isErr()) {
throw mapApplicationError(result.unwrapErr())
}
this.presenter.present(result.value)
return this.presenter.getViewModel()
}
}
```
#### Why This Matters
1. **Dependency Rule**: Inner layers (use cases) cannot depend on outer layers (presenters)
2. **Testability**: Use cases can be tested without mocking presenters
3. **Flexibility**: Same use case can work with different presenters
4. **Separation of Concerns**: Use cases do business logic, presenters do transformation
#### What Must Be Fixed
**All use cases in the codebase must be updated to:**
1. **Remove** the `output: UseCaseOutputPort<T>` constructor parameter
2. **Return** `Result<T, E>` directly from `execute()`
3. **Remove** all `this.output.present()` calls
**All controllers must be updated to:**
1. **Call** the use case and get the Result
2. **Pass** `result.value` to the presenter's `.present()` method
3. **Return** the presenter's `.getViewModel()`
This is the **single source of truth** for correct Clean Architecture in this project.

View File

@@ -0,0 +1,51 @@
# Authentication and Authorization (Shared Contract)
This document defines the shared, cross-app contract for authentication and authorization.
It does not define Next.js routing details or NestJS guard wiring.
App-specific documents:
- API enforcement: [`docs/architecture/api/AUTH_FLOW.md`](docs/architecture/api/AUTH_FLOW.md:1)
- Website UX flow: [`docs/architecture/website/WEBSITE_AUTH_FLOW.md`](docs/architecture/website/WEBSITE_AUTH_FLOW.md:1)
## 1) Core principle (non-negotiable)
The API is the single source of truth for:
- who the actor is
- what the actor is allowed to do
The website may improve UX. It does not enforce security.
## 2) Authentication (strict)
Authentication answers:
- who is this actor
Rules:
- the actor identity is derived from the authenticated session
- the client must never be allowed to claim an identity
## 3) Authorization (strict)
Authorization answers:
- is this actor allowed to perform this action
Rules:
- authorization is enforced in the API
- the website may hide or disable UI, but cannot enforce correctness
See: [`docs/architecture/api/AUTHORIZATION.md`](docs/architecture/api/AUTHORIZATION.md:1)
## 4) Shared terminology (hard)
- Guard: API enforcement mechanism
- Blocker: website UX prevention mechanism
Shared contract: [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1)

View File

@@ -0,0 +1,60 @@
# Blockers and Guards (Shared Contract)
This document defines the **shared contract** for Blockers (website UX) and Guards (API enforcement).
If a more specific document conflicts with this one, this shared contract wins.
Related:
- API authorization: [`docs/architecture/api/AUTHORIZATION.md`](docs/architecture/api/AUTHORIZATION.md:1)
- Website delivery-layer contract: [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1)
## 1) Core principle (non-negotiable)
Guards enforce. Blockers prevent.
- Guards protect the system.
- Blockers protect the UX.
There are no exceptions.
## 2) Definitions (strict)
### 2.1 Guard (API)
A Guard is an **API mechanism** that enforces access or execution rules.
If a Guard denies execution, the request does not reach application logic.
### 2.2 Blocker (Website)
A Blocker is a **website mechanism** that prevents an action from being executed.
Blockers exist solely to improve UX and reduce unnecessary requests.
Blockers are not security.
## 3) Responsibility split (hard boundary)
| Aspect | Blocker (Website) | Guard (API) |
|---|---|---|
| Purpose | Prevent execution | Enforce rules |
| Security | ❌ No | ✅ Yes |
| Authority | ❌ Best-effort | ✅ Final |
| Reversible | ✅ Yes | ❌ No |
| Failure effect | UX feedback | HTTP error |
## 4) Naming rules (hard)
- Website uses `*Blocker`.
- API uses `*Guard`.
- Never mix the terms.
- Never implement Guards in the website.
- Never implement Blockers in the API.
## 5) Final rule
If it must be enforced, it is a Guard.
If it only prevents UX mistakes, it is a Blocker.

View File

@@ -1,468 +1,47 @@
Clean Architecture Application Services, Use Cases, Ports, and Data Flow (Strict, Final)
# Clean Architecture Data Flow (Shared Contract)
This document defines the final, non-ambiguous Clean Architecture setup for the project.
This document defines the **shared** data-flow rules that apply across all delivery applications.
It explicitly covers:
• Use Cases vs Application Services
• Input & Output Ports (and what does not exist)
• API responsibilities
• Frontend responsibilities
• Naming, placement, and dependency rules
• End-to-end flow with concrete paths and code examples
It does not contain app-specific rules.
There are no hybrid concepts, no overloaded terms, and no optional interpretations.
App-specific contracts:
- Core: [`docs/architecture/core/CORE_DATA_FLOW.md`](docs/architecture/core/CORE_DATA_FLOW.md:1)
- API: [`docs/architecture/api/API_DATA_FLOW.md`](docs/architecture/api/API_DATA_FLOW.md:1)
- Website: [`docs/architecture/website/WEBSITE_DATA_FLOW.md`](docs/architecture/website/WEBSITE_DATA_FLOW.md:1)
1. Architectural Layers (Final)
## 1) Dependency rule (non-negotiable)
Domain → Business truth
Application → Use Cases + Application Services
Adapters → API, Persistence, External Systems
Frontend → UI, View Models, UX logic
Dependencies point inward.
Only dependency-inward is allowed.
```text
Delivery apps → adapters → core
```
Core never depends on delivery apps.
2. Domain Layer (Core / Domain)
## 2) Cross-boundary mapping rule
What lives here
• Entities (classes)
• Value Objects (classes)
• Domain Services (stateless business logic)
• Domain Events
• Domain Errors / Invariants
What NEVER lives here
• DTOs
• Models
• Ports
• Use Cases
• Application Services
• Framework code
3. Application Layer (Core / Application)
The Application Layer has two distinct responsibilities:
1. Use Cases business decisions
2. Application Services orchestration of multiple use cases
4. Use Cases (Application / Use Cases)
Definition
A Use Case represents one business intent.
If data crosses a boundary, it is mapped.
Examples:
• CreateLeague
• ApproveSponsorship
• CompleteDriverOnboarding
Rules
• A Use Case:
• contains business logic
• enforces invariants
• operates on domain entities
• communicates ONLY via ports
• A Use Case:
• does NOT orchestrate multiple workflows
• does NOT know HTTP, UI, DB, queues
- HTTP Request DTO is mapped to Core input.
- Core result is mapped to HTTP Response DTO.
Structure
## 3) Ownership rule
core/racing/application/use-cases/
└─ CreateLeagueUseCase.ts
Each layer owns its data shapes.
Example
- Core owns domain and application models.
- API owns HTTP DTOs.
- Website owns ViewData and ViewModels.
export class CreateLeagueUseCase {
constructor(
private readonly leagueRepository: LeagueRepositoryPort,
private readonly output: CreateLeagueOutputPort
) {}
No layer re-exports another layers models as-is across a boundary.
execute(input: CreateLeagueInputPort): void {
// business rules & invariants
## 4) Non-negotiable rules
const league = League.create(input.name, input.maxMembers);
this.leagueRepository.save(league);
1. Core contains business truth.
2. Delivery apps translate and enforce.
3. Adapters implement ports.
this.output.presentSuccess(league.id);
}
}
5. Ports (Application / Ports)
The Only Two Kinds of Ports
Everything crossing the Application boundary is a Port.
Input Ports
Input Ports describe what a use case needs.
export interface CreateLeagueInputPort {
readonly name: string;
readonly maxMembers: number;
}
Rules:
• Interfaces only
• No behavior
• No validation logic
Output Ports
Output Ports describe how a use case emits outcomes.
export interface CreateLeagueOutputPort {
presentSuccess(leagueId: string): void;
presentFailure(reason: string): void;
}
Rules:
• No return values
• No getters
• No state
• Use methods, not result objects
6. Application Services (Application / Services)
Definition
An Application Service orchestrates multiple Use Cases.
It exists because:
• No single Use Case should know the whole workflow
• Orchestration is not business logic
Rules
• Application Services:
• call multiple Use Cases
• define execution order
• handle partial failure & compensation
• Application Services:
• do NOT contain business rules
• do NOT modify entities directly
Structure
core/racing/application/services/
└─ LeagueSetupService.ts
Example (with Edge Cases)
export class LeagueSetupService {
constructor(
private readonly createLeague: CreateLeagueUseCase,
private readonly createSeason: CreateSeasonUseCase,
private readonly assignOwner: AssignLeagueOwnerUseCase,
private readonly notify: SendLeagueWelcomeNotificationUseCase
) {}
execute(input: LeagueSetupInputPort): void {
const leagueId = this.createLeague.execute(input);
try {
this.createSeason.execute({ leagueId });
this.assignOwner.execute({ leagueId, ownerId: input.ownerId });
this.notify.execute({ leagueId, ownerId: input.ownerId });
} catch (error) {
// compensation / rollback logic
throw error;
}
}
}
Edge cases that belong ONLY here:
• Partial failure handling
• Workflow order
• Optional steps
• Retry / idempotency logic
7. API Layer (apps/api)
Responsibilities
• Transport (HTTP)
• Validation (request shape)
• Mapping to Input Ports
• Calling Application Services
• Adapting Output Ports
Structure
apps/api/leagues/
├─ LeagueController.ts
├─ presenters/
│ └─ CreateLeaguePresenter.ts
└─ dto/
├─ CreateLeagueRequestDto.ts
└─ CreateLeagueResponseDto.ts
API Presenter (Adapter)
export class CreateLeaguePresenter implements CreateLeagueOutputPort {
private response!: CreateLeagueResponseDto;
presentSuccess(leagueId: string): void {
this.response = { success: true, leagueId };
}
presentFailure(reason: string): void {
this.response = { success: false, errorMessage: reason };
}
getResponse(): CreateLeagueResponseDto {
return this.response;
}
}
8. Frontend Layer (apps/website)
The frontend layer contains UI-specific data shapes. None of these cross into the Core.
Important: `apps/website` is a Next.js delivery app with SSR/RSC. This introduces one additional presentation concept to keep server/client boundaries correct.
There are four distinct frontend data concepts:
1. API DTOs (transport)
2. Command Models (user input / form state)
3. View Models (client-only presentation classes)
4. ViewData (template input, serializable)
8.1 API DTOs (Transport Contracts)
API DTOs represent exact HTTP contracts exposed by the backend.
They are usually generated from OpenAPI or manually mirrored.
apps/website/lib/dtos/
└─ CreateLeagueResponseDto.ts
Rules:
• Exact mirror of backend response
• No UI logic
• No derived values
• Never used directly by components
8.2 Command Models (User Input / Form State)
Command Models represent user intent before submission.
They are frontend-only and exist to manage:
• form state
• validation feedback
• step-based wizards
They are NOT:
• domain objects
• API DTOs
• View Models
apps/website/lib/commands/
└─ CreateLeagueCommandModel.ts
Rules:
• Classes (stateful)
• May contain client-side validation
• May contain UX-specific helpers (step validation, dirty flags)
• Must expose a method to convert to an API Request DTO
Example responsibility:
• hold incomplete or invalid user input
• guide the user through multi-step flows
• prepare data for submission
Command Models:
• are consumed by components
• are passed into services
• are never sent directly over HTTP
8.3 View Models (UI Display State)
View Models represent fully prepared UI state after data is loaded.
apps/website/lib/view-models/
└─ CreateLeagueViewModel.ts
Rules:
• Classes only
• UI logic allowed (formatting, labels, derived flags)
• No domain logic
• No mutation after construction
SSR/RSC rule (website-only):
• View Models are client-only and MUST NOT cross server-to-client boundaries.
• Templates MUST NOT accept View Models.
8.4 Website Presenters (DTO → ViewModel)
Website Presenters are pure mappers.
export class CreateLeaguePresenter {
present(dto: CreateLeagueResponseDto): CreateLeagueViewModel {
return new CreateLeagueViewModel(dto);
}
}
Rules:
• Input: API DTOs
• Output: View Models
• No side effects
• No API calls
8.5 Website Services (Orchestration)
Website Services orchestrate:
• Command Models
• API Client calls
• Presenter mappings
export class LeagueService {
async createLeague(command: CreateLeagueCommandModel): Promise<CreateLeagueViewModel> {
const dto = await this.api.createLeague(command.toRequestDto());
return this.presenter.present(dto);
}
}
Rules:
• Services accept Command Models
• Services return View Models
• Components never call API clients directly
View Models (UI State)
apps/website/lib/view-models/
└─ CreateLeagueViewModel.ts
export class CreateLeagueViewModel {
constructor(private readonly dto: CreateLeagueResponseDto) {}
get message(): string {
return this.dto.success
? 'League created successfully'
: this.dto.errorMessage ?? 'Creation failed';
}
}
Rules:
• Classes only
• UI logic allowed
• No domain logic
Website Presenter (DTO → ViewModel)
export class CreateLeaguePresenter {
present(dto: CreateLeagueResponseDto): CreateLeagueViewModel {
return new CreateLeagueViewModel(dto);
}
}
Website Service (Orchestration)
export class LeagueService {
constructor(
private readonly api: LeaguesApiClient,
private readonly presenter: CreateLeaguePresenter
) {}
async createLeague(input: unknown): Promise<CreateLeagueViewModel> {
const dto = await this.api.createLeague(input);
return this.presenter.present(dto);
}
}
9. Full End-to-End Flow (Final)
UI Component
→ Website Service
→ API Client
→ HTTP Request DTO
→ API Controller
→ Application Service
→ Use Case(s)
→ Domain
→ Output Port
→ API Presenter
→ HTTP Response DTO
→ Website Presenter
→ View Model
→ UI
10. Final Non-Negotiable Rules
• Core knows ONLY Ports + Domain
• Core has NO Models, DTOs, or ViewModels
• API talks ONLY to Application Services
• Controllers NEVER call Use Cases directly
• Frontend Components see ONLY ViewData (Templates) or ViewModels (Client orchestrators)
• API DTOs never cross into Templates
• View Models never cross into Templates
11. Final Merksatz
Use Cases decide.
Application Services orchestrate.
Adapters translate.
UI presents.
If a class violates more than one of these roles, it is incorrectly placed.
8.3.1 ViewData (Template Input)
ViewData is the only allowed input for Templates in `apps/website`.
Definition:
• JSON-serializable data structure
• Contains only primitives/arrays/plain objects
• Ready to render: Templates perform no formatting and no derived computation
Rules:
• ViewData is built in client code from:
1) Page DTO (initial SSR-safe render)
2) ViewModel (post-hydration enhancement)
• ViewData MUST NOT contain ViewModel instances or Display Object instances.
Authoritative details:
• [docs/architecture/website/VIEW_DATA.md](docs/architecture/website/VIEW_DATA.md:1)
• [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1)

View File

@@ -0,0 +1,84 @@
# Enums (Shared Contract)
This document defines how enums are modeled, placed, and used across the system.
Enums are frequently a source of architectural leakage. This contract removes ambiguity.
## 1) Core principle (non-negotiable)
Enums represent knowledge.
Knowledge must live where it is true.
## 2) Enum categories (strict)
There are four and only four valid enum categories:
1. Domain enums
2. Application workflow enums
3. Transport enums (HTTP contracts)
4. UI enums (website-only)
## 3) Domain enums
Definition:
- business meaning
- affects rules or invariants
Placement:
- `core/**`
Rule:
- domain enums MUST NOT cross a delivery boundary
## 4) Application workflow enums
Definition:
- internal workflow coordination
- not business truth
Placement:
- `core/**`
Rule:
- workflow enums MUST remain internal
## 5) Transport enums
Definition:
- constrain HTTP contracts
Placement:
- `apps/api/**` and `apps/website/**` (as transport representations)
Rules:
- transport enums are copies, not reexports of domain enums
- transport enums MUST NOT be used inside Core
## 6) UI enums
Definition:
- website presentation or interaction state
Placement:
- `apps/website/**`
Rule:
- UI enums MUST NOT leak into API or Core
## 7) Final rule
If an enum crosses a boundary, it is in the wrong place.

View File

@@ -1,315 +1,37 @@
# Feature Availability (Modes + Feature Flags)
# Feature Availability (Shared Contract)
This document defines a clean, consistent system for enabling/disabling functionality across:
- API endpoints
- Website links/navigation
- Website components
This document defines the shared, cross-app system for enabling and disabling capabilities.
It is designed to support:
- test mode
- maintenance mode
- disabling features due to risk/issues
- coming soon features
- future super admin flag management
Feature availability is not authorization.
It is aligned with the hard separation of responsibilities in `Blockers & Guards`:
- Frontend uses Blockers (UX best-effort)
- Backend uses Guards (authoritative enforcement)
Shared contract:
See: docs/architecture/BLOCKER_GUARDS.md
- Blockers and Guards: [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1)
---
## 1) Core Principle
## 1) Core principle (non-negotiable)
Availability is decided once, then applied in multiple places.
- Backend Guards enforce availability for correctness and security.
- Frontend Blockers reflect availability for UX, but must never be relied on for enforcement.
- API Guards enforce availability.
- Website Blockers reflect availability for UX.
If it must be enforced, it is a Guard.
If it only improves UX, it is a Blocker.
## 2) Capability model (strict)
---
Inputs to evaluation:
## 2) Definitions (Canonical Vocabulary)
### 2.1 Operational Mode (system-level)
A small, global state representing operational posture.
Recommended enum:
- normal
- maintenance
- test
Operational Mode is:
- authoritative in backend
- typically environment-scoped
- required for rapid response (maintenance must be runtime-changeable)
### 2.2 Feature State (capability-level)
A per-feature state machine (not a boolean).
Recommended enum:
- enabled
- disabled
- coming_soon
- hidden
Semantics:
- enabled: feature is available and advertised
- disabled: feature exists but must not be used (safety kill switch)
- coming_soon: may be visible in UI as teaser, but actions are blocked
- hidden: not visible/advertised; actions are blocked (safest default)
### 2.3 Capability
A named unit of functionality (stable key) used consistently across API + website.
Examples:
- races.create
- payments.checkout
- sponsor.portal
- stewarding.protests
A capability key is a contract.
### 2.4 Action Type
Availability decisions vary by the type of action:
- view: read-only operations (pages, GET endpoints)
- mutate: state-changing operations (POST/PUT/PATCH/DELETE)
---
## 3) Policy Model (What Exists)
### 3.1 FeatureAvailabilityPolicy (single evaluation model)
One evaluation function produces a decision.
Inputs:
- environment (dev/test/prod)
- operationalMode (normal/maintenance/test)
- capabilityKey (string)
- actionType (view/mutate)
- actorContext (anonymous/authenticated; roles later)
- operational mode (normal, maintenance, test)
- capability key (stable string)
- action type (view, mutate)
- actor context (anonymous, authenticated)
Outputs:
- allow: boolean
- publicReason: one of maintenance | disabled | coming_soon | hidden | not_configured
- uxHint: optional { messageKey, redirectPath, showTeaser }
The same decision model is reused by:
- API Guard enforcement
- Website navigation visibility
- Website component rendering/disablement
- allow or deny
- a public reason (maintenance, disabled, coming_soon, hidden, not_configured)
### 3.2 Precedence (where values come from)
To avoid “mystery behavior”, use strict precedence:
## 3) Non-negotiable rules
1. runtime overrides (highest priority)
2. build-time environment configuration
3. code defaults (lowest priority, should be safe: hidden/disabled)
1. Default is deny unless explicitly enabled.
2. The API is authoritative.
3. The website is UX-only.
Rationale:
- runtime overrides enable emergency response without rebuild
- env config enables environment-specific defaults
- code defaults keep behavior deterministic if config is missing
---
## 4) Evaluation Rules (Deterministic, Explicit)
### 4.1 Maintenance mode rules
Maintenance must be able to block the platform fast and consistently.
Default behavior:
- mutate actions: denied unless explicitly allowlisted
- view actions: allowed only for a small allowlist (status page, login, health, static public routes)
This creates a safe “fail closed” posture.
Optional refinement:
- define a maintenance allowlist for critical reads (e.g., dashboards for operators)
### 4.2 Test mode rules
Test mode should primarily exist in non-prod, and should be explicit in prod.
Recommended behavior:
- In prod, test mode should not be enabled accidentally.
- In test environments, test mode may:
- enable test-only endpoints
- bypass external integrations (through adapters)
- relax rate limits
- expose test banners in UI (Blocker-level display)
### 4.3 Feature state rules (per capability)
Given a capability state:
- enabled:
- allow view + mutate (subject to auth/roles)
- visible in UI
- coming_soon:
- allow view of teaser pages/components
- deny mutate and deny sensitive reads
- visible in UI with Coming Soon affordances
- disabled:
- deny view + mutate
- hidden in nav by default
- hidden:
- deny view + mutate
- never visible in UI
Note:
- “disabled” and “hidden” are both blocked; the difference is UI and information disclosure.
### 4.4 Missing configuration
If a capability is not configured:
- treat as hidden (fail closed)
- optionally log a warning (server-side)
---
## 5) Enforcement Mapping (Where Each Requirement Lives)
This section is the “wiring contract” across layers.
### 5.1 API endpoints (authoritative)
- Enforce via Backend Guards (NestJS CanActivate).
- Endpoints must declare the capability they require.
Mapping to HTTP:
- maintenance: 503 Service Unavailable (preferred for global maintenance)
- disabled/hidden: 404 Not Found (avoid advertising unavailable capabilities)
- coming_soon: 404 Not Found publicly, or 409 Conflict internally if you want explicit semantics for trusted clients later
Guideline:
- External clients should not get detailed feature availability information unless explicitly intended.
### 5.2 Website links / navigation (UX)
- Enforce via Frontend Blockers.
- Hide links when state is disabled/hidden.
- For coming_soon, show link but route to teaser page or disable with explanation.
Rules:
- Never assume hidden in UI equals enforced on server.
- UI should degrade gracefully (API may still block).
### 5.3 Website components (UX)
- Use Blockers to:
- hide components for hidden/disabled
- show teaser content for coming_soon
- disable buttons or flows for coming_soon/disabled, with consistent messaging
Recommendation:
- Provide a single reusable component (FeatureBlocker) that consumes policy decisions and renders:
- children when allowed
- teaser when coming_soon
- null or fallback when disabled/hidden
---
## 6) Build-Time vs Runtime (Clean, Predictable)
### 6.1 Build-time flags (require rebuild/redeploy)
What they are good for:
- preventing unfinished UI code from shipping in a bundle
- cutting entire routes/components from builds for deterministic releases
Limitations:
- NEXT_PUBLIC_* values are compiled into the client bundle; changing them does not update clients without rebuild.
Use build-time flags for:
- experimental UI
- “not yet shipped” components/routes
- simplifying deployments (pre-launch vs alpha style gating)
### 6.2 Runtime flags (no rebuild)
What they are for:
- maintenance mode
- emergency disable for broken endpoints
- quickly hiding risky features
Runtime flags must be available to:
- API Guards (always)
- Website SSR/middleware optionally
- Website client optionally (for UX only)
Key tradeoff:
- runtime access introduces caching and latency concerns
- treat runtime policy reads as cached, fast, and resilient
Recommended approach:
- API is authoritative source of runtime policy
- website can optionally consume a cached policy snapshot endpoint
---
## 7) Storage and Distribution (Now + Future Super Admin)
### 7.1 Now (no super admin UI)
Use a single “policy snapshot” stored in one place and read by the API, with caching.
Options (in priority order):
1. Remote KV/DB-backed policy snapshot (preferred for true runtime changes)
2. Environment variable JSON (simpler, but changes require restart/redeploy)
3. Static config file in repo (requires rebuild/redeploy)
### 7.2 Future (super admin UI)
Super admin becomes a writer to the same store.
Non-negotiable:
- The storage schema must be stable and versioned.
Recommended schema (conceptual):
- policyVersion
- operationalMode
- capabilities: map of capabilityKey -> featureState
- allowlists: maintenance view/mutate allowlists
- optional targeting rules later (by role/user)
---
## 8) Data Flow (Conceptual)
```mermaid
flowchart LR
UI[Website UI] --> FB[Frontend Blockers]
FB --> PC[Policy Client]
UI --> API[API Request]
API --> FG[Feature Guard]
FG --> AS[API Application Service]
AS --> UC[Core Use Case]
PC --> PS[Policy Snapshot]
FG --> PS
```
Interpretation:
- Website reads policy for UX (best-effort).
- API enforces policy (authoritative) before any application logic.
---
## 9) Implementation Checklist (For Code Mode)
Backend (apps/api):
- Define capability keys and feature states as shared types in a local module.
- Create FeaturePolicyService that resolves the current policy snapshot (cached).
- Add FeatureFlagGuard (or FeatureAvailabilityGuard) that:
- reads required capability metadata for an endpoint
- evaluates allow/deny with actionType
- maps denial to the chosen HTTP status codes
Frontend (apps/website):
- Add a small PolicyClient that fetches policy snapshot from API (optional for phase 1).
- Add FeatureBlocker component for consistent UI behavior.
- Centralize navigation link definitions and filter them via policy.
Ops/Config:
- Define how maintenance mode is toggled (KV/DB entry or config endpoint restricted to operators later).
- Ensure defaults are safe (fail closed).
---
## 10) Non-Goals (Explicit)
- This system is not an authorization system.
- Roles/permissions are separate (but can be added as actorContext inputs later).
- Blockers never replace Guards.

View File

@@ -1,115 +0,0 @@
# File Structure
## Core
```
core/ # * Business- & Anwendungslogik (framework-frei)
├── shared/ # * Gemeinsame Core-Bausteine
│ ├── domain/ # * Domain-Basistypen
│ │ ├── Entity.ts # * Basisklasse für Entities
│ │ ├── ValueObject.ts # * Basisklasse für Value Objects
│ │ └── DomainError.ts # * Domain-spezifische Fehler
│ └── application/
│ └── ApplicationError.ts # * Use-Case-/Application-Fehler
├── racing/ # * Beispiel-Domain (Bounded Context)
│ ├── domain/ # * Fachliche Wahrheit
│ │ ├── entities/ # * Aggregate Roots & Entities
│ │ │ ├── League.ts # * Aggregate Root
│ │ │ └── Race.ts # * Entity
│ │ ├── value-objects/ # * Unveränderliche Fachwerte
│ │ │ └── LeagueName.ts # * Beispiel VO
│ │ ├── services/ # * Domain Services (Regeln, kein Ablauf)
│ │ │ └── ChampionshipScoringService.ts # * Regel über mehrere Entities
│ │ └── errors/ # * Domain-Invariantenfehler
│ │ └── RacingDomainError.ts
│ │
│ └── application/ # * Anwendungslogik
│ ├── ports/ # * EINZIGE Schnittstellen des Cores
│ │ ├── input/ # * Input Ports (Use-Case-Grenzen)
│ │ │ └── CreateLeagueInputPort.ts
│ │ └── output/ # * Output Ports (Use-Case-Ergebnisse)
│ │ └── CreateLeagueOutputPort.ts
│ │
│ ├── use-cases/ # * Einzelne Business-Intents
│ │ └── CreateLeagueUseCase.ts
│ │
│ └── services/ # * Application Services (Orchestrierung)
│ └── LeagueSetupService.ts # * Koordiniert mehrere Use Cases
```
## Adapters
```
adapters/ # * Alle äußeren Implementierungen
├── persistence/ # * Datenhaltung
│ ├── typeorm/ # Konkrete DB-Technologie
│ │ ├── entities/ # ORM-Entities (nicht Domain!)
│ │ └── repositories/ # * Implementieren Core-Ports
│ │ └── LeagueRepository.ts
│ └── inmemory/ # Test-/Dev-Implementierungen
│ └── LeagueRepository.ts
├── notifications/ # Externe Systeme
│ └── EmailNotificationAdapter.ts # Implementiert Notification-Port
├── logging/ # Logging / Telemetrie
│ └── ConsoleLoggerAdapter.ts # Adapter für Logger-Port
└── bootstrap/ # Initialisierung / Seeding
└── EnsureInitialData.ts # App-Start-Logik
```
## API
```
apps/api/ # * Delivery Layer (HTTP)
├── app.module.ts # * Framework-Zusammenbau
├── leagues/ # * Feature-Modul
│ ├── LeagueController.ts # * HTTP → Application Service
│ │
│ ├── dto/ # * Transport-DTOs (HTTP)
│ │ ├── CreateLeagueRequestDto.ts # * Request-Shape
│ │ └── CreateLeagueResponseDto.ts # * Response-Shape
│ │
│ └── presenters/ # * Output-Port-Adapter
│ └── CreateLeaguePresenter.ts # * Core Output → HTTP Response
└── shared/ # API-spezifisch
└── filters/ # Exception-Handling
```
## Frontend
```
apps/website/ # * Frontend (UI)
├── app/ # * Next.js Routen
│ └── leagues/ # * Page-Level
│ └── page.tsx
├── components/ # * Reine UI-Komponenten
│ └── LeagueForm.tsx
├── lib/
│ ├── api/ # * HTTP-Client
│ │ └── LeaguesApiClient.ts # * Gibt NUR API DTOs zurück
│ │
│ ├── dtos/ # * API-Vertrags-Typen
│ │ └── CreateLeagueResponseDto.ts
│ │
│ ├── commands/ # * Command Models (Form State)
│ │ └── CreateLeagueCommandModel.ts
│ │
│ ├── presenters/ # * DTO → ViewModel Mapper
│ │ └── CreateLeaguePresenter.ts
│ │
│ ├── view-models/ # * UI-State
│ │ └── CreateLeagueViewModel.ts
│ │
│ ├── services/ # * Frontend-Orchestrierung
│ │ └── LeagueService.ts
│ │
│ └── blockers/ # UX-Schutz (Throttle, Submit)
│ ├── SubmitBlocker.ts
│ └── ThrottleBlocker.ts
```

View File

@@ -0,0 +1,56 @@
# Repository Structure (Shared Contract)
This document defines the **physical repository structure**.
It describes **where** code lives, not what responsibilities are.
## 1) Top-level layout (strict)
```text
core/  business logic (framework-free)
adapters/  reusable infrastructure implementations
apps/  delivery applications (API, website)
docs/  documentation
tests/  cross-app tests
```
## 2) Meaning of each top-level folder
### 2.1 `core/`
The Core contains domain and application logic.
See [`docs/architecture/core/CORE_FILE_STRUCTURE.md`](docs/architecture/core/CORE_FILE_STRUCTURE.md:1).
### 2.2 `adapters/`
Adapters are **reusable outer-layer implementations**.
Rules:
- adapters implement Core ports
- adapters contain technical details (DB, external systems)
- adapters do not define HTTP routes
See [`docs/architecture/shared/ADAPTERS.md`](docs/architecture/shared/ADAPTERS.md:1).
### 2.3 `apps/`
Apps are **delivery mechanisms**.
This repo has (at minimum):
- `apps/api` (HTTP API)
- `apps/website` (Next.js website)
See:
- [`docs/architecture/api/API_FILE_STRUCTURE.md`](docs/architecture/api/API_FILE_STRUCTURE.md:1)
- [`docs/architecture/website/WEBSITE_FILE_STRUCTURE.md`](docs/architecture/website/WEBSITE_FILE_STRUCTURE.md:1)
## 3) Non-negotiable rules
1. Business truth lives in `core/`.
2. `apps/*` are delivery apps; they translate and enforce.
3. `adapters/*` implement ports and contain technical details.

View File

@@ -1,640 +0,0 @@
# Unified Authentication & Authorization Architecture
## Executive Summary
This document defines a **clean, predictable, and secure** authentication and authorization architecture that eliminates the current "fucking unpredictable mess" by establishing clear boundaries between server-side and client-side responsibilities.
## Current State Analysis
### What's Wrong
1. **Confusing Layers**: Middleware, RouteGuards, AuthGuards, Blockers, Gateways - unclear hierarchy
2. **Mixed Responsibilities**: Server and client both doing similar checks inconsistently
3. **Inconsistent Patterns**: Some routes use middleware, some use guards, some use both
4. **Role Confusion**: Frontend has role logic that should be server-only
5. **Debugging Nightmare**: Multiple layers with unclear flow
### What's Actually Working
1. **API Guards**: Clean NestJS pattern with `@Public()`, `@RequireRoles()`
2. **Basic Middleware**: Route protection works at edge
3. **Auth Context**: Session management exists
4. **Permission Model**: Documented in AUTHORIZATION.md
## Core Principle: Server as Source of Truth
**Golden Rule**: The API server is the **single source of truth** for authentication and authorization. The client is a dumb terminal that displays what the server allows.
### Server-Side Responsibilities (API)
#### 1. Authentication
-**Session Validation**: Verify JWT/session cookies
-**Identity Resolution**: Who is this user?
-**Token Management**: Issue, refresh, revoke tokens
-**UI Redirects**: Never redirect, return 401/403
#### 2. Authorization
-**Role Verification**: Check user roles against requirements
-**Permission Evaluation**: Check capabilities (view/mutate)
-**Scope Resolution**: Determine league/sponsor/team context
-**Access Denial**: Return 401/403 with clear messages
-**Client State**: Never trust client-provided identity
#### 3. Data Filtering
-**Filter sensitive data**: Remove fields based on permissions
-**Scope-based queries**: Only return data user can access
-**Client-side filtering**: Never rely on frontend to hide data
### Client-Side Responsibilities (Website)
#### 1. UX Enhancement
-**Loading States**: Show "Verifying authentication..."
-**Redirects**: Send unauthenticated users to login
-**UI Hiding**: Hide buttons/links user can't access
-**Feedback**: Show "Access denied" messages
-**Security**: Never trust client checks for security
#### 2. Session Management
-**Session Cache**: Store session in context
-**Auto-refresh**: Fetch session on app load
-**Logout Flow**: Clear local state, call API logout
-**Role Logic**: Don't make decisions based on roles
#### 3. Route Protection
-**Middleware**: Basic auth check at edge
-**Layout Guards**: Verify session before rendering
-**Page Guards**: Additional verification (defense in depth)
-**Authorization**: Don't check permissions, let API fail
## Clean Architecture Layers
```
┌─────────────────────────────────────────────────────────────┐
│ USER REQUEST │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 1. EDGE MIDDLEWARE (Next.js) │
│ • Check for session cookie │
│ • Public routes: Allow through │
│ • Protected routes: Require auth cookie │
│ • Redirect to login if no cookie │
│ • NEVER check roles here │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. API REQUEST (with session cookie) │
│ • NestJS AuthenticationGuard extracts user from session │
│ • Attaches user identity to request │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. API AUTHORIZATION GUARD │
│ • Check route metadata: @Public(), @RequireRoles() │
│ • Evaluate permissions based on user identity │
│ • Return 401 (unauthenticated) or 403 (forbidden) │
│ • NEVER redirect, NEVER trust client identity │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. API CONTROLLER │
│ • Execute business logic │
│ • Filter data based on permissions │
│ • Return appropriate response │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 5. CLIENT RESPONSE HANDLING │
│ • 200: Render data │
│ • 401: Redirect to login with returnTo │
│ • 403: Show "Access denied" message │
│ • 404: Show "Not found" (for non-disclosure) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 6. COMPONENT RENDERING │
│ • Layout guards: Verify session exists │
│ • Route guards: Show loading → content or redirect │
│ • UI elements: Hide buttons user can't use │
└─────────────────────────────────────────────────────────────┘
```
## Implementation: Clean Route Protection
### Step 1: Simplify Middleware (Edge Layer)
**File**: `apps/website/middleware.ts`
```typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Edge Middleware - Simple and Predictable
*
* Responsibilities:
* 1. Allow public routes (static assets, auth pages, discovery)
* 2. Check for session cookie on protected routes
* 3. Redirect to login if no cookie
* 4. Let everything else through (API handles authorization)
*/
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. Always allow static assets and API routes
if (
pathname.startsWith('/_next/') ||
pathname.startsWith('/api/') ||
pathname.match(/\.(svg|png|jpg|jpeg|gif|webp|ico|css|js)$/)
) {
return NextResponse.next();
}
// 2. Define public routes (no auth required)
const publicRoutes = [
'/',
'/auth/login',
'/auth/signup',
'/auth/forgot-password',
'/auth/reset-password',
'/auth/iracing',
'/auth/iracing/start',
'/auth/iracing/callback',
'/leagues',
'/drivers',
'/teams',
'/leaderboards',
'/races',
'/sponsor/signup',
];
// 3. Check if current route is public
const isPublic = publicRoutes.includes(pathname) ||
publicRoutes.some(route => pathname.startsWith(route + '/'));
if (isPublic) {
// Special handling: redirect authenticated users away from auth pages
const hasAuthCookie = request.cookies.has('gp_session');
const authRoutes = ['/auth/login', '/auth/signup', '/auth/forgot-password', '/auth/reset-password'];
if (authRoutes.includes(pathname) && hasAuthCookie) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
// 4. Protected routes: require session cookie
const hasAuthCookie = request.cookies.has('gp_session');
if (!hasAuthCookie) {
const loginUrl = new URL('/auth/login', request.url);
loginUrl.searchParams.set('returnTo', pathname);
return NextResponse.redirect(loginUrl);
}
// 5. User has cookie, let them through
// API will handle actual authorization
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|_next/data|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|mp4|webm|mov|avi)$).*)',
],
};
```
### Step 2: Clean Layout Guards (Client Layer)
**File**: `apps/website/lib/guards/AuthLayout.tsx`
```typescript
'use client';
import { ReactNode, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth/AuthContext';
import { LoadingState } from '@/components/shared/LoadingState';
interface AuthLayoutProps {
children: ReactNode;
requireAuth?: boolean;
redirectTo?: string;
}
/**
* AuthLayout - Client-side session verification
*
* Responsibilities:
* 1. Verify user session exists
* 2. Show loading state while checking
* 3. Redirect to login if no session
* 4. Render children if authenticated
*
* Does NOT check permissions - that's the API's job
*/
export function AuthLayout({
children,
requireAuth = true,
redirectTo = '/auth/login'
}: AuthLayoutProps) {
const router = useRouter();
const { session, loading } = useAuth();
useEffect(() => {
if (!requireAuth) return;
// If done loading and no session, redirect
if (!loading && !session) {
const returnTo = window.location.pathname;
router.push(`${redirectTo}?returnTo=${encodeURIComponent(returnTo)}`);
}
}, [loading, session, router, requireAuth, redirectTo]);
// Show loading state
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-deep-graphite">
<LoadingState message="Verifying authentication..." />
</div>
);
}
// Show nothing while redirecting (or show error if not redirecting)
if (requireAuth && !session) {
return null;
}
// Render protected content
return <>{children}</>;
}
```
### Step 3: Role-Based Layout (Client Layer)
**File**: `apps/website/lib/guards/RoleLayout.tsx`
```typescript
'use client';
import { ReactNode, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth/AuthContext';
import { LoadingState } from '@/components/shared/LoadingState';
interface RoleLayoutProps {
children: ReactNode;
requiredRoles: string[];
redirectTo?: string;
}
/**
* RoleLayout - Client-side role verification
*
* Responsibilities:
* 1. Verify user session exists
* 2. Show loading state
* 3. Redirect if no session OR insufficient role
* 4. Render children if authorized
*
* Note: This is UX enhancement. API is still source of truth.
*/
export function RoleLayout({
children,
requiredRoles,
redirectTo = '/auth/login'
}: RoleLayoutProps) {
const router = useRouter();
const { session, loading } = useAuth();
useEffect(() => {
if (loading) return;
// No session? Redirect
if (!session) {
const returnTo = window.location.pathname;
router.push(`${redirectTo}?returnTo=${encodeURIComponent(returnTo)}`);
return;
}
// Has session but wrong role? Redirect
if (requiredRoles.length > 0 && !requiredRoles.includes(session.role || '')) {
// Could redirect to dashboard or show access denied
router.push('/dashboard');
return;
}
}, [loading, session, router, requiredRoles, redirectTo]);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-deep-graphite">
<LoadingState message="Verifying access..." />
</div>
);
}
if (!session || (requiredRoles.length > 0 && !requiredRoles.includes(session.role || ''))) {
return null;
}
return <>{children}</>;
}
```
### Step 4: Usage Examples
#### Public Route (No Protection)
```typescript
// app/leagues/page.tsx
export default function LeaguesPage() {
return <LeaguesList />;
}
```
#### Authenticated Route
```typescript
// app/dashboard/layout.tsx
import { AuthLayout } from '@/lib/guards/AuthLayout';
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<AuthLayout requireAuth={true}>
<div className="min-h-screen bg-deep-graphite">
{children}
</div>
</AuthLayout>
);
}
// app/dashboard/page.tsx
export default function DashboardPage() {
// No additional auth checks needed - layout handles it
return <DashboardContent />;
}
```
#### Role-Protected Route
```typescript
// app/admin/layout.tsx
import { RoleLayout } from '@/lib/guards/RoleLayout';
export default function AdminLayout({ children }: { children: ReactNode }) {
return (
<RoleLayout requiredRoles={['owner', 'admin']}>
<div className="min-h-screen bg-deep-graphite">
{children}
</div>
</RoleLayout>
);
}
// app/admin/page.tsx
export default function AdminPage() {
// No additional checks - layout handles role verification
return <AdminDashboard />;
}
```
#### Scoped Route (League Admin)
```typescript
// app/leagues/[id]/settings/layout.tsx
import { AuthLayout } from '@/lib/guards/AuthLayout';
import { LeagueAccessGuard } from '@/components/leagues/LeagueAccessGuard';
export default function LeagueSettingsLayout({
children,
params
}: {
children: ReactNode;
params: { id: string };
}) {
return (
<AuthLayout requireAuth={true}>
<LeagueAccessGuard leagueId={params.id}>
<div className="min-h-screen bg-deep-graphite">
{children}
</div>
</LeagueAccessGuard>
</AuthLayout>
);
}
```
### Step 5: API Guard Cleanup
**File**: `apps/api/src/domain/auth/AuthorizationGuard.ts`
```typescript
import { CanActivate, ExecutionContext, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthorizationService } from './AuthorizationService';
import { PUBLIC_ROUTE_METADATA_KEY } from './Public';
import { REQUIRE_ROLES_METADATA_KEY, RequireRolesMetadata } from './RequireRoles';
type AuthenticatedRequest = {
user?: { userId: string };
};
@Injectable()
export class AuthorizationGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly authorizationService: AuthorizationService,
) {}
canActivate(context: ExecutionContext): boolean {
const handler = context.getHandler();
const controllerClass = context.getClass();
// 1. Check if route is public
const isPublic = this.reflector.getAllAndOverride<{ public: true } | undefined>(
PUBLIC_ROUTE_METADATA_KEY,
[handler, controllerClass],
)?.public ?? false;
if (isPublic) {
return true;
}
// 2. Get required roles
const rolesMetadata = this.reflector.getAllAndOverride<RequireRolesMetadata | undefined>(
REQUIRE_ROLES_METADATA_KEY,
[handler, controllerClass],
) ?? null;
// 3. Get user identity from request (set by AuthenticationGuard)
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
const userId = request.user?.userId;
// 4. Deny if not authenticated
if (!userId) {
throw new UnauthorizedException('Authentication required');
}
// 5. If no roles required, allow
if (!rolesMetadata || rolesMetadata.anyOf.length === 0) {
return true;
}
// 6. Check if user has required role
const userRoles = this.authorizationService.getRolesForUser(userId);
const hasAnyRole = rolesMetadata.anyOf.some((r) => userRoles.includes(r));
if (!hasAnyRole) {
throw new ForbiddenException(`Access requires one of: ${rolesMetadata.anyOf.join(', ')}`);
}
return true;
}
}
```
### Step 6: Client Error Handling
**File**: `apps/website/lib/api/client.ts`
```typescript
/**
* API Client with unified error handling
*/
export async function apiFetch(url: string, options: RequestInit = {}) {
const response = await fetch(url, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
// Handle authentication errors
if (response.status === 401) {
// Session expired or invalid
window.location.href = '/auth/login?returnTo=' + encodeURIComponent(window.location.pathname);
throw new Error('Authentication required');
}
// Handle authorization errors
if (response.status === 403) {
const error = await response.json().catch(() => ({ message: 'Access denied' }));
throw new Error(error.message || 'You do not have permission to access this resource');
}
// Handle not found
if (response.status === 404) {
throw new Error('Resource not found');
}
// Handle server errors
if (response.status >= 500) {
throw new Error('Server error. Please try again later.');
}
return response;
}
```
## Benefits of This Architecture
### 1. **Clear Responsibilities**
- Server: Security and authorization
- Client: UX and user experience
### 2. **Predictable Flow**
```
User → Middleware → API → Guard → Controller → Response → Client
```
### 3. **Easy Debugging**
- Check middleware logs
- Check API guard logs
- Check client session state
### 4. **Secure by Default**
- API never trusts client
- Client never makes security decisions
- Defense in depth without confusion
### 5. **Scalable**
- Easy to add new routes
- Easy to add new roles
- Easy to add new scopes
## Migration Plan
### Phase 1: Clean Up Middleware (1 day)
- [ ] Simplify `middleware.ts` to only check session cookie
- [ ] Remove role logic from middleware
- [ ] Define clear public routes list
### Phase 2: Create Clean Guards (2 days)
- [ ] Create `AuthLayout` component
- [ ] Create `RoleLayout` component
- [ ] Create `ScopedLayout` component
- [ ] Remove old RouteGuard/AuthGuard complexity
### Phase 3: Update Route Layouts (2 days)
- [ ] Update all protected route layouts
- [ ] Remove redundant page-level checks
- [ ] Test all redirect flows
### Phase 4: API Guard Enhancement (1 day)
- [ ] Ensure all endpoints have proper decorators
- [ ] Add missing `@Public()` or `@RequireRoles()`
- [ ] Test 401/403 responses
### Phase 5: Documentation & Testing (1 day)
- [ ] Update all route protection docs
- [ ] Create testing checklist
- [ ] Verify all scenarios work
## Testing Checklist
### Unauthenticated User
- [ ] `/dashboard` → Redirects to `/auth/login?returnTo=/dashboard`
- [ ] `/profile` → Redirects to `/auth/login?returnTo=/profile`
- [ ] `/admin` → Redirects to `/auth/login?returnTo=/admin`
- [ ] `/leagues` → Works (public)
- [ ] `/auth/login` → Works (public)
### Authenticated User (Regular)
- [ ] `/dashboard` → Works
- [ ] `/profile` → Works
- [ ] `/admin` → Redirects to `/dashboard` (no role)
- [ ] `/leagues` → Works (public)
- [ ] `/auth/login` → Redirects to `/dashboard`
### Authenticated User (Admin)
- [ ] `/dashboard` → Works
- [ ] `/profile` → Works
- [ ] `/admin` → Works
- [ ] `/admin/users` → Works
- [ ] `/leagues` → Works (public)
### Session Expiry
- [ ] Navigate to protected route with expired session → Redirect to login
- [ ] Return to original route after login → Works
### API Direct Calls
- [ ] Call protected endpoint without auth → 401
- [ ] Call admin endpoint without role → 403
- [ ] Call public endpoint → 200
## Summary
This architecture eliminates the "fucking unpredictable mess" by:
1. **One Source of Truth**: API server handles all security
2. **Clear Layers**: Middleware → API → Guards → Controller
3. **Simple Client**: UX enhancement only, no security decisions
4. **Predictable Flow**: Always the same path for every request
5. **Easy to Debug**: Each layer has one job
The result: **Clean, predictable, secure authentication and authorization that just works.**

View File

@@ -0,0 +1,51 @@
# Blockers (Website UX)
This document defines **Blockers** as UX-only prevention mechanisms in the website.
Shared contract: [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1)
## 1) Definition
A Blocker is a website mechanism that prevents an action from being executed.
Blockers exist solely to improve UX and reduce unnecessary requests.
Blockers are not security.
## 2) 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 website
- be treated as best-effort helpers
## 3) Restrictions
Blockers MUST NOT:
- enforce security
- claim authorization
- block access permanently
- replace API Guards
- make assumptions about backend state
## 4) Common Blockers
- SubmitBlocker
- ThrottleBlocker
- NavigationBlocker
- FeatureBlocker
## 5) Canonical placement
- `apps/website/lib/blockers/**`

View File

@@ -1,154 +0,0 @@
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

@@ -50,11 +50,10 @@ Blockers exist to prevent UX mistakes.
- Blockers may reduce unnecessary requests.
- The API still enforces rules.
See [`BLOCKER_GUARDS.md`](docs/architecture/website/BLOCKER_GUARDS.md:1).
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).
## 6) Canonical placement in this repo
- `apps/website/lib/blockers/**`
- `apps/website/lib/hooks/**`
- `apps/website/lib/command-models/**`

View File

@@ -1,14 +1,20 @@
# Login Flow State Machine Architecture
# Login Flow State Machine (Strict)
## 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
This document defines the canonical, deterministic login flow controller for the website.
## Solution: State Machine Pattern
Authoritative website contract:
### State Definitions
- [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1)
## 1) Core rule
Login flow logic MUST be deterministic.
The same inputs MUST produce the same state and the same next action.
## 2) State machine definition (strict)
### 2.1 State definitions
```typescript
enum LoginState {
@@ -19,7 +25,7 @@ enum LoginState {
}
```
### State Transition Table
### 2.2 State transition table
| Current State | Session | ReturnTo | Next State | Action |
|---------------|---------|----------|------------|--------|
@@ -29,7 +35,7 @@ enum LoginState {
| UNAUTHENTICATED | exists | any | POST_AUTH_REDIRECT | Redirect to returnTo |
| AUTHENTICATED_WITHOUT_PERMISSIONS | exists | any | POST_AUTH_REDIRECT | Redirect to returnTo |
### Class-Based Controller
### 2.3 Class-based controller
```typescript
class LoginFlowController {
@@ -57,7 +63,7 @@ class LoginFlowController {
return this.state;
}
// Pure function - returns action, doesn't execute
// Pure function - returns action, does not execute
getNextAction(): LoginAction {
switch (this.state) {
case LoginState.UNAUTHENTICATED:
@@ -71,7 +77,7 @@ class LoginFlowController {
}
}
// Called after authentication
// Transition called after authentication
transitionToPostAuth(): void {
if (this.session) {
this.state = LoginState.POST_AUTH_REDIRECT;
@@ -80,15 +86,14 @@ class LoginFlowController {
}
```
### Benefits
## 3) Non-negotiable rules
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
1. The controller MUST be constructed from explicit inputs only.
2. The controller MUST NOT perform side effects.
3. Side effects (routing) MUST be executed outside the controller.
4. The controller MUST be unit-tested per transition.
### Usage in Login Page
## 4) Usage in login page (example)
```typescript
export default function LoginPage() {
@@ -129,4 +134,4 @@ export default function LoginPage() {
}
```
This eliminates all the unpredictable behavior and makes the flow testable and maintainable.
This pattern ensures deterministic behavior and makes the flow testable.

View File

@@ -0,0 +1,46 @@
# Authentication UX Flow (Website)
This document defines how the website handles authentication from a UX perspective.
Shared contract:
- [`docs/architecture/shared/AUTH_CONTRACT.md`](docs/architecture/shared/AUTH_CONTRACT.md:1)
Authoritative website contract:
- [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1)
## 1) Website role (strict)
The website:
- redirects unauthenticated users to login
- hides or disables UI based on best-effort session knowledge
The website does not enforce security.
## 2) Canonical website flow
```text
Request
Website routing
API requests with credentials
API enforces authentication and authorization
Website renders result or redirects
```
## 3) Non-negotiable rules
1. The website MUST NOT claim authorization.
2. The website MUST NOT trust client state for enforcement.
3. Every write still relies on the API to accept or reject.
Related:
- Website blockers: [`docs/architecture/website/BLOCKERS.md`](docs/architecture/website/BLOCKERS.md:1)
- Client state rules: [`docs/architecture/website/CLIENT_STATE.md`](docs/architecture/website/CLIENT_STATE.md:1)

View File

@@ -189,7 +189,7 @@ See [`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:1).
- The website MUST NOT enforce security.
- The API enforces authentication and authorization.
See [`BLOCKER_GUARDS.md`](docs/architecture/website/BLOCKER_GUARDS.md:1).
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)

View File

@@ -0,0 +1,65 @@
# Website Data Flow (Strict)
This document defines the **apps/website** data flow.
Authoritative contract: [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
Website scope:
- `apps/website/**`
## 1) Website role
The website is a **delivery layer**.
It renders truth from the API and forwards user intent to the API.
## 2) Read flow
```text
RSC page.tsx
PageQuery
API client (infra)
API Transport DTO
Page DTO
Presenter (client)
ViewModel (optional)
Presenter (client)
ViewData
Template
```
## 3) Write flow
All writes enter through **Server Actions**.
```text
User intent
Server Action
Command Model / Request DTO
API
Revalidation
RSC reload
```
## 4) Non-negotiable rules
1. Templates accept ViewData only.
2. Page Queries do not format.
3. Presenters do not call the API.
4. Client state is UI-only.

View File

@@ -0,0 +1,50 @@
# Website File Structure (Strict)
This document defines the canonical **physical** structure for `apps/website/**`.
It describes where code lives, not the full behavioral rules.
Authoritative contract:
- [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1)
## 1) High-level layout
```text
apps/website/
app/  Next.js routes (RSC pages, layouts, server actions)
templates/  template components (ViewData only)
lib/  website code (clients, services, view-models, etc.)
```
## 2) `apps/website/app/` (routing)
Routes are implemented via Next.js App Router.
Rules:
- server `page.tsx` does composition only
- templates are pure
- writes enter via server actions
See [`docs/architecture/website/WEBSITE_RSC_PRESENTATION.md`](docs/architecture/website/WEBSITE_RSC_PRESENTATION.md:1).
## 3) `apps/website/lib/` (website internals)
Canonical folders (existing in this repo):
```text
apps/website/lib/
api/  API clients
infrastructure/  technical concerns
services/  UI orchestration (read-only and write orchestration)
page-queries/  server composition
types/  API transport DTOs
view-models/  client-only classes
display-objects/  deterministic formatting helpers
command-models/  transient form models
blockers/  UX-only prevention
hooks/  React-only helpers
di/  client-first DI integration
```