# Website architecture audit: Template vs Client, Server Actions, Mutations, React Query

Contract baseline: [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1)

## 0) Executive summary

The codebase is **not uniformly wrong**; it is **inconsistent**.

Two competing architectures coexist:

1) **Contract-first RSC** (good):
   - `app/**/page.tsx` calls PageQuery → builds ViewData → renders Template (examples below).
   - Writes go through Server Actions → Mutations (examples below).

2) **React Query client app** (problematic when used for writes):
   - Client modules execute writes via React Query `useMutation()` directly against services (or placeholders).
   - This **violates the strict write boundary** in [`docs/architecture/website/FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:20).

Additionally, some Templates violate determinism rules (notably `toLocaleString()` usage) which can cause SSR/hydration mismatches.

My recommendation to get to a clean codebase:

- Keep React Query **only for client-side reads** (and optional cache invalidation after server actions), but **ban React Query mutations for writes**.
- Enforce a single write path everywhere: **Client intent → Server Action → Mutation → Service → API Client**.
- Align Templates with determinism and responsibility rules: Templates render; they do not format using runtime-locale APIs.

---

## 1) What the code is doing today (evidence)

### 1.1 A good reference read flow (RSC → PageQuery → Template)

- Server page calls PageQuery and renders Template: [`DashboardPage()`](apps/website/app/dashboard/page.tsx:5)
- Template is a pure renderer receiving ViewData: [`DashboardTemplate()`](apps/website/templates/DashboardTemplate.tsx:21)

This matches the contract read flow in [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:181).

### 1.2 A good reference write flow (Server Action → Mutation)

- Server action calls a mutation and revalidates: [`updateUserStatus()`](apps/website/app/admin/actions.ts:25)
- Server action calls a mutation and revalidates: [`updateProfileAction()`](apps/website/app/profile/actions.ts:8)
- Server action calls a mutation and revalidates: [`completeOnboardingAction()`](apps/website/app/onboarding/completeOnboardingAction.ts:18)

This matches the contract write flow in [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:236) and the mutation guidance in [`docs/architecture/website/MUTATIONS.md`](docs/architecture/website/MUTATIONS.md:15).

### 1.3 React Query is definitely used, and it is used for writes

React Query is a dependency: [`apps/website/package.json`](apps/website/package.json:16)

React Query provider exists: [`QueryClientProvider()`](apps/website/lib/providers/QueryClientProvider.tsx:16)

There is a standardized abstraction that promotes React Query usage for both reads and writes:
- [`usePageData()`](apps/website/lib/page/usePageData.ts:25)
- [`usePageMutation()`](apps/website/lib/page/usePageData.ts:114)

Concrete contract violations:

- Client-side writes via React Query mutation calling a service:
  - [`useMutation()`](apps/website/app/leagues/[id]/schedule/admin/LeagueAdminSchedulePageClient.tsx:52) calling [`leagueService.createAdminScheduleRace()`](apps/website/app/leagues/[id]/schedule/admin/LeagueAdminSchedulePageClient.tsx:69) and similar write methods.
  - This is a direct breach of the strict rule: “All writes MUST enter through Next.js Server Actions” in [`docs/architecture/website/FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:20).

- Duplicate write paths exist for onboarding:
  - Server action exists: [`completeOnboardingAction()`](apps/website/app/onboarding/completeOnboardingAction.ts:18)
  - But a React Query mutation hook also calls a service directly: [`useCompleteOnboarding()`](apps/website/hooks/onboarding/useCompleteOnboarding.ts:10)

- Some “mutation hooks” are placeholders (dangerous because they normalize the wrong path):
  - [`useJoinTeam()`](apps/website/hooks/team/useJoinTeam.ts:10)
  - [`useLeaveTeam()`](apps/website/hooks/team/useLeaveTeam.ts:9)
  - These currently do no server action and no API call, but they establish the pattern that client-side “writes” live in React Query.

### 1.4 Template vs Client responsibilities are inconsistent

A clean example of the intended split exists:

- Server page acts as data orchestrator: [`Page()`](apps/website/app/drivers/page.tsx:6)
- Client entry-point owns state and navigation: [`DriversPageClient()`](apps/website/app/drivers/DriversPageClient.tsx:30)
- Template is basically composition/rendering: [`DriversTemplate()`](apps/website/templates/DriversTemplate.tsx:38)

But the actual boundaries are blurry in at least two ways:

1) Templates contain non-deterministic formatting:
   - [`toLocaleString()`](apps/website/templates/DriversTemplate.tsx:65)
   - This violates strict determinism constraints in [`docs/architecture/website/VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:58) and [`docs/architecture/website/DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:71).

2) `app/**/*PageClient.tsx` sometimes becomes “everything”: state, rendering, presentation model transformation, and workaround ViewModel creation.
   - Example: “TODO wtf we have builders for this” comment while doing ViewData → ViewModel ad-hoc mapping in [`LeaguesPageClient()`](apps/website/app/leagues/LeaguesPageClient.tsx:418) and specifically the conversion at [`LeagueSummaryViewModel`](apps/website/app/leagues/LeaguesPageClient.tsx:380).

### 1.5 There are plain bugs/anti-patterns that amplify the mess

- `useState()` used as an effect runner (should be `useEffect()`):
  - [`useState()`](apps/website/app/leagues/LeaguesPageClient.tsx:297)
  - This is not an architecture issue per se, but it is “noise” that makes architecture discussions harder.

---

## 2) The root causes (why this became a mess)

1) **Write boundary not enforced strongly enough**
   - There are good server actions + mutation examples, but nothing stops new client code from doing `useMutation()` → `service.write()`.

2) **Templates and ViewData rules are not consistently applied**
   - Determinism rules (no `Intl.*`, no `toLocale*`) are strict in docs, but not enforced in Templates.

3) **Two read strategies are both used**
   - RSC PageQueries exist.
   - React Query hooks exist.
   - Without a clear “when to use which,” teams will mix-and-match.

---

## 3) Recommended target architecture (single coherent system)

### 3.1 Writes: one true path

Hard rule (enforced):

- Client code MUST NOT perform write HTTP requests.
- Client code MUST NOT call write methods on services that hit the API.
- Client code MUST NOT use React Query `useMutation()` for writes.

Canonical write flow (matches docs):

```mermaid
flowchart TD
  UI[Client component intent] --> SA[Server Action]
  SA --> M[Mutation]
  M --> S[Service]
  S --> AC[API Client]
  AC --> API[Backend API]
  SA --> RV[Revalidate Path]
```

Docs reference: [`docs/architecture/website/FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:20)

### 3.2 Reads: choose a default, allow an exception

Default:

- If the route can be rendered on the server: `page.tsx` → PageQuery → ViewDataBuilder → Template.

Exception:

- Use React Query for client-only interactive islands that truly need client-driven data refresh.
- React Query should be treated as **a client state/cache** of authoritative API truth, not the truth itself.

Canonical read flow option A (RSC):

```mermaid
flowchart TD
  Page[page.tsx] --> PQ[PageQuery]
  PQ --> S[Service]
  S --> AC[API Client]
  PQ --> B[ViewDataBuilder]
  B --> T[Template]
```

Docs reference: [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:181)

Canonical read flow option B (client-only island):

```mermaid
flowchart TD
  C[Client component] --> Q[React Query useQuery]
  Q --> S[Service or API Client read]
  C --> T[Template]
```

Important: This is only acceptable for reads. Writes still go through server actions.

---

## 4) Concrete refactor plan (incremental, low-risk)

### Step 1: Establish explicit conventions

1) `app/**/page.tsx` MUST be server by default and must only orchestrate:
   - call PageQuery
   - map Result → `notFound()`/`redirect()`/error UI
   - build ViewData using Builder
   - render Template

2) `app/**/*PageClient.tsx` is the ONLY place allowed to:
   - use `useRouter()`
   - hold UI state
   - call server actions

3) `templates/**/*Template.tsx` MUST:
   - accept ViewData (or client-only “presentation props” like handler functions)
   - contain no `Intl.*` or `toLocale*` formatting
   - contain no data fetching

### Step 2: Fix strict correctness violations first (determinism)

- Remove runtime-locale formatting from templates:
  - Replace [`toLocaleString()`](apps/website/templates/DriversTemplate.tsx:65) and [`toLocaleString()`](apps/website/templates/DriversTemplate.tsx:66) with deterministic formatting.
  - Preferred: ViewData already contains display-ready strings (built via ViewDataBuilder), or deterministic Display Objects.
  - Docs reference: [`docs/architecture/website/VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:58).

### Step 3: Move client-side writes behind server actions

Primary offender to migrate:

- Replace React Query write mutations in [`LeagueAdminSchedulePageClient()`](apps/website/app/leagues/[id]/schedule/admin/LeagueAdminSchedulePageClient.tsx:22)
  - Replace [`useMutation()`](apps/website/app/leagues/[id]/schedule/admin/LeagueAdminSchedulePageClient.tsx:52) with server actions (publish/save/delete) that call Mutations.
  - Then the client component calls those server actions and refreshes view (either `router.refresh()` or route revalidation).

Secondary offenders:

- Consolidate onboarding writes to one path:
  - Keep server actions: [`completeOnboardingAction()`](apps/website/app/onboarding/completeOnboardingAction.ts:18) and [`generateAvatarsAction()`](apps/website/app/onboarding/generateAvatarsAction.ts:12)
  - Delete or rewrite client hook [`useCompleteOnboarding()`](apps/website/hooks/onboarding/useCompleteOnboarding.ts:10) to call server action instead of a service.

### Step 4: Clean up presentation model transforms

- Eliminate ad-hoc “ViewData → ViewModel” conversions in client code:
  - Example to refactor: the conversion at [`LeagueSummaryViewModel`](apps/website/app/leagues/LeaguesPageClient.tsx:380).
  - Replace with either:
    - a proper ViewModelBuilder (if the component truly needs ViewModel semantics), or
    - adjust the component (`LeagueCard`) to accept ViewData shape directly.

### Step 5: Reduce React Query surface area (optional, but recommended)

- Deprecate the abstraction that encourages client-side writes:
  - Keep [`usePageData()`](apps/website/lib/page/usePageData.ts:25) for reads if desired.
  - Remove or forbid [`usePageMutation()`](apps/website/lib/page/usePageData.ts:114).

### Step 6: Add guardrails to prevent regression

Minimal guardrails that pay off:

1) ESLint rule or convention: forbid `useMutation` imports except in `__tests__` or an allowlist.
2) ESLint rule: forbid service write methods from being imported into client modules (harder, but possible with path rules).
3) ESLint rule: forbid `toLocaleString()` and `Intl.*` in Templates.

---

## 5) Answers to your specific concerns

### 5.1 `*Template.tsx` vs `*Client.tsx`

- This split is **correct** and already present in multiple routes.
- The inconsistency is that some “Client” files are doing too much (transform, render, and mutate).
- The fix is to treat `*PageClient.tsx` as wiring + state + calling server actions, and keep templates deterministic.

### 5.2 Server actions vs mutations

- The codebase already shows the correct approach:
  - [`updateUserStatus()`](apps/website/app/admin/actions.ts:25)
  - [`updateProfileAction()`](apps/website/app/profile/actions.ts:8)

- The “mess” is client-side writes bypassing this.

### 5.4 Command Models: Are they useless?

No. Command Models (e.g., [`LoginCommandModel`](apps/website/lib/command-models/auth/LoginCommandModel.ts)) remain valuable as **optional UX helpers**.

- **Purpose**: They manage transient form state and perform client-side UX validation.
- **Relationship to Server Actions**: They prepare the DTO that is passed *to* the Server Action.
- **Boundary**: They belong to the client-side "intent collection" phase. Once the Server Action is called, the Command Model's job is done.
- **Redundancy**: They are **not redundant** with ViewModels. ViewModels are for the **read path** (API -> UI), while Command Models are for the **write path** (UI -> API).

### 5.5 Identified places for Command Model usage

The following components currently use complex React state for forms and should be refactored to use Command Models:

1.  **[`CreateLeagueWizard.tsx`](apps/website/app/leagues/create/CreateLeagueWizard.tsx)**: Needs a `CreateLeagueCommandModel` to manage multi-step wizard state and validation.
2.  **[`LeagueAdminSchedulePageClient.tsx`](apps/website/app/leagues/[id]/schedule/admin/LeagueAdminSchedulePageClient.tsx)**: Should use a `RaceScheduleCommandModel` for the track/car/date fields.
3.  **[`ProtestDetailPageClient.tsx`](apps/website/app/leagues/[id]/stewarding/protests/[protestId]/ProtestDetailPageClient.tsx)**: Should fully leverage the existing [`ProtestDecisionCommandModel`](apps/website/lib/command-models/protests/ProtestDecisionCommandModel.ts).
4.  **[`SponsorSignupPage`](apps/website/app/sponsor/signup/page.tsx)**: Needs a `SponsorSignupCommandModel` to clean up the `formData` and `errors` state.

---

## 6) Opinion: ViewData vs ViewModel (what actually makes sense)

Your original intuition is sound:

- **ViewData = server output** (JSON, deterministic, template-ready)
- **ViewModel = client-only** (state, derived UI helpers, never serialized)

The awkward part is: the repo currently contains **conflicting written contracts**.

- Strict statement: “Templates accept ViewData only” in [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:372).
- But [`docs/architecture/website/VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:34) shows an example where a Template receives a ViewModel in client code.

### My recommendation: keep Templates ViewData-only, keep ViewModels client-only

I would enforce this as the single, clean rule:

1) **Templates accept ViewData only** (one prop-shape; server and client use the same templates).
2) **Client components may use ViewModels internally** for state and derived values.
3) **The boundary is explicit**: client code converts `ViewModel -> ViewData` right before rendering a Template.

Why this is the least-bad option:

- It prevents a “two template APIs” world (one for ViewData, one for ViewModel) which tends to explode complexity.
- It aligns with RSC constraints (serializable data across the server/client boundary).
- It keeps ViewModels useful: they can own derived UI state and display logic, but the render surface is still a plain object.

Concrete example of why you feel “ViewModels are pushed away by ViewData”:

- In [`LeaguesPageClient()`](apps/website/app/leagues/LeaguesPageClient.tsx:418) you’re holding ViewData and then doing a hand-rolled mapping to a “ViewModel-like” shape at [`LeagueSummaryViewModel`](apps/website/app/leagues/LeaguesPageClient.tsx:380). That’s a symptom of **missing builders and an unclear boundary**, not a reason to abandon ViewModels.

## 7) Implementation Status (Audit Results)

The following cleanups have been implemented:

1.  **Deterministic Formatting**: Removed `toLocaleString()` and `toLocaleDateString()` from all templates. Introduced `NumberDisplay` and `DateDisplay` display objects for deterministic formatting.
2.  **Server Action Migration**: Migrated the schedule administration logic in `LeagueAdminSchedulePageClient.tsx` from React Query `useMutation` to Next.js Server Actions.
3.  **Onboarding Consolidation**: Updated onboarding hooks to use server actions instead of direct service calls.
4.  **ViewModel Builders**: Introduced `LeagueSummaryViewModelBuilder` to eliminate ad-hoc ViewData -> ViewModel mapping in `LeaguesPageClient.tsx`.
5.  **React Query Deprecation**: Deprecated `usePageMutation` in `usePageData.ts`.
6.  **Guardrails**: Added `gridpilot-rules/no-use-mutation-in-client` ESLint rule to prevent future React Query write usage.
7.  **Command Models**:
    - Created [`RaceScheduleCommandModel`](apps/website/lib/command-models/leagues/RaceScheduleCommandModel.ts) and integrated it into [`LeagueAdminSchedulePageClient.tsx`](apps/website/app/leagues/[id]/schedule/admin/LeagueAdminSchedulePageClient.tsx).
    - Created [`SponsorSignupCommandModel`](apps/website/lib/command-models/sponsors/SponsorSignupCommandModel.ts) and integrated it into [`SponsorSignupPage`](apps/website/app/sponsor/signup/page.tsx).
    - Ensured [`LeagueWizardCommandModel`](apps/website/lib/command-models/leagues/LeagueWizardCommandModel.ts) is ready for future refactoring of the complex wizard.

## 8) Next Steps for the Team

- Continue migrating remaining `useMutation` usages to Server Actions.
- Ensure all new templates follow the ViewData-only rule.
- Use `NumberDisplay` and `DateDisplay` for all UI formatting.

