---
name: 'Wordle Clone Architecture Spine'
type: architecture-spine
purpose: build-substrate
altitude: feature
paradigm: 'SPA + REST API, stateless server'
scope: 'Wordle Clone v1 — web application, solver engine, word bank'
status: final
created: '2026-07-07'
updated: '2026-07-07'
binds: ['FR-1'..'FR-19', 'UJ-1'..'UJ-5']
sources: ['prds/prd-wordle-2026-07-07/prd.md']
companions: []
---

# Architecture Spine — Wordle Clone

## Design Paradigm

**SPA + stateless REST API.** The client is a single-page application loaded once; all subsequent interaction is REST calls against a stateless server. The server holds no session state — every request carries the context it needs (word ID, attempt number, skill metric). Deterministic computation replaces stored state wherever possible (daily word ID is a pure function of the date).

```
Browser (React SPA) ─── REST ─── Server (Express, stateless)
                                    │
                                    ├── data/word-bank.json (static)
                                    └── solver output (pre-computed, static)
```

**Layer map:** `client/` (presentation + game logic) → `server/` (REST API, guess validation, word selection) → `data/` (word bank, solver rankings). `shared/` holds TypeScript types consumed by all layers. `solver/` is an offline script, not part of the runtime.

## Invariants & Rules

### AD-1 — Stateless server

- **Binds:** server/
- **Prevents:** server-side session state, in-memory game tracking, per-player storage on the server
- **Rule:** Every request carries all context needed to process it. The server stores nothing between requests. `[ADOPTED]`

### AD-2 — Deterministic daily word

- **Binds:** GET /api/daily, server/src/daily.ts
- **Prevents:** stored daily-word state, cron jobs for rotation, clock-drift between requests
- **Rule:** Daily word ID = `(daysSinceEpoch(EST/EDT) mod targetWordCount) + 1`. Same date always maps to same word for every player. Timezone: America/New_York. `[ADOPTED]`

### AD-3 — TypeScript everywhere

- **Binds:** all
- **Prevents:** multi-language drift, duplicate type definitions, solver/API type mismatches
- **Rule:** All code (client, server, solver) is TypeScript. Shared types in `shared/` are the single source of truth for API contracts, word bank shape, and game types. `[ADOPTED]`

### AD-4 — Word bank as static data

- **Binds:** server/, solver/, data/
- **Prevents:** database dependency, migration overhead, runtime word-list mutation
- **Rule:** The word bank is a single static JSON file (`data/word-bank.json`) loaded at server startup. Shape: `{ guessable: string[], targets: { id: number, word: string, difficulty: number }[] }`. The `difficulty` field is a single aggregated score (e.g., average attempts across all solver algorithms). The solver script produces this shape; the server consumes it. Per-algorithm attempt counts are solver-internal and not exposed in the bank. `[ADOPTED]`

### AD-5 — Guess validation server-side only

- **Binds:** server/src/validate.ts, POST /api/guess
- **Prevents:** word bank leaking to client, client-side validation drift from server
- **Rule:** The guessable word list never leaves the server. Every guess is validated server-side against the full word bank. Invalid guesses return `{ valid: false }` without consuming an attempt. **Exception:** On game over (loss, `tryNo=6 && !correct`), the correct word string is returned in the guess response — this is a single-word reveal, not a list leak. `[ADOPTED]`

### AD-6 — Feedback scoring on server

- **Binds:** server/src/feedback.ts, POST /api/guess
- **Prevents:** client and server computing feedback differently, scoring rule divergence
- **Rule:** The Feedback Scoring Rule (greens first, yellows to remaining count, excess gray) is implemented once on the server. The client only renders the colors it receives. `[ADOPTED]`

### AD-7 — Client owns game state

- **Binds:** client/
- **Prevents:** server needing to track attempts, win/loss, or game progress
- **Rule:** The client tracks: current attempt number, guess history, game-over detection, rules-seen cookie, lifetime statistics, and skill metric. All persist to browser storage. The server never knows what attempt the player is on except what `tryNo` the client sends. `[ADOPTED]`

### AD-8 — Word selection by Gaussian sampling

- **Binds:** POST /api/play-again, server/src/play-again.ts
- **Prevents:** fixed-difficulty buckets, deterministic word ordering in play-again mode
- **Rule:** Play Again word selection is weighted random: words near the player's skill level are more likely, harder words become more likely as skill improves. Specific distribution (Gaussian or other) is an implementation detail. No repeat-word tracking — the target set is large enough that repeats are negligible. `[ADOPTED]`

### AD-9 — Monorepo with shared types

- **Binds:** all
- **Prevents:** type mismatches across package boundaries, duplicated interface definitions
- **Rule:** Single repository with packages: `client/`, `server/`, `solver/`, `shared/`, `data/`. `shared/` exports TypeScript interfaces for API request/response shapes, word bank structure, and game types. Every package imports types from `shared/`; no package defines its own copy of a shared type. `[ADOPTED]`

### AD-10 — Typed API contracts before implementation

- **Binds:** shared/src/api.ts, client/src/api.ts, server/src/routes/
- **Prevents:** field-name divergence (tryNo vs attemptNumber), type mismatches (wordId as string vs number), different response shapes for same endpoint
- **Rule:** Every API endpoint has a concrete TypeScript interface in `shared/src/api.ts` defining request body, response body (success and error shapes), and HTTP method+path. No endpoint implementation begins before its interface is committed to `shared/`. `[ADOPTED]`

### AD-11 — Feedback wire format

- **Binds:** shared/src/api.ts, server/src/feedback.ts, client/src/components/Grid.tsx
- **Prevents:** client and server encoding colors differently (string literals vs numeric codes vs per-letter objects)
- **Rule:** Feedback colors on the wire are single-character strings: `'g'` (green, correct position), `'y'` (yellow, wrong position), `'x'` (gray, absent or excess). The `colors` field in the guess response is a 5-element array of these characters. `[ADOPTED]`

### AD-12 — Single persistence key with typed schema

- **Binds:** client/src/hooks/usePersistence.ts
- **Prevents:** localStorage key collisions, schema drift between hooks, unversioned state corruption
- **Rule:** All browser-persisted state lives under a single `localStorage` key (`wordle-state`). The value is a JSON-serialized `PersistedState` interface defined in `shared/src/game.ts`. One hook (`usePersistence`) owns all read/write access; `useStats` and `useGame` consume it, never touch storage directly. `[ADOPTED]`

## Consistency Conventions

| Concern | Convention |
| --- | --- |
| Naming (files, functions, interfaces) | camelCase functions/variables, PascalCase types/interfaces/components, kebab-case files |
| API contracts | Request/response shapes defined as TypeScript interfaces in `shared/src/api.ts` |
| Error shapes | API errors return `{ error: string }` with appropriate HTTP status (400 invalid guess, 404 unknown wordId) |
| Responsive design | CSS media queries, mobile-first. Target: usable at 375px width (small phone) through desktop |
| State mutation | Client state is immutable-style — each guess produces a new state object; React renders from state |
| Network errors | Client displays error message on request failure; player can retry. No offline mode for v1 |
| Logging | Server logs requests to stdout (method, path, status, latency). No structured logging framework for v1 |

## Stack

| Name | Version |
| --- | --- |
| TypeScript | ^6.x |
| Node.js | ^24 LTS |
| React | ^19.x |
| Vite | ^8.x |
| Express | ^5.x |
| `shared/` types package | workspace |

## Structural Seed

### Context diagram

```mermaid
graph LR
    B[Browser<br/>React SPA] -->|REST| S[Express Server]
    S -->|reads at startup| WB[data/word-bank.json]
    SV[Solver script] -->|writes| WB
    S -->|serves static files| B
    B -->|stores| LS[Browser localStorage]
```

### Source tree

```text
wordle/
  client/               # React + Vite SPA
    src/
      screens/          # Screen1 (Rules), Screen2 (Game), Screen3 (Results)
      components/       # Grid, Keyboard, Stats, shared UI
      hooks/            # useGame, useStats, usePersistence
      api.ts            # fetch wrappers for /api/*
    index.html
  server/               # Express REST API
    src/
      index.ts          # app entry, static file serving
      routes/
        daily.ts        # GET /api/daily
        guess.ts        # POST /api/guess
        play-again.ts   # POST /api/play-again
      feedback.ts       # green/yellow/gray scoring logic
      validate.ts       # word bank lookup
      daily-word.ts     # date → wordId function
    package.json
  solver/               # offline pre-compute
    src/
      index.ts          # orchestrates solvers, writes rankings
      entropy.ts        # entropy-based solver
      minimax.ts        # minimax solver (or alternative)
    package.json
  shared/               # TypeScript types
    src/
      api.ts            # request/response interfaces
      word-bank.ts      # WordBank, TargetWord, etc.
      game.ts           # Guess, Feedback, Colors, etc.
    package.json
  data/                 # generated / static data
    word-bank.json      # guessable + targets with difficulty scores
  package.json          # workspace root
```

## Capability → Architecture Map

| Capability / Area | Lives in | Governed by |
| --- | --- | --- |
| FR-1 First-visit detection | client/src/hooks/usePersistence.ts | AD-7 |
| FR-2 Rules display | client/src/screens/Screen1.tsx | AD-7 |
| FR-3 Grid rendering | client/src/components/Grid.tsx | AD-7 |
| FR-4 Keyboard | client/src/components/Keyboard.tsx | AD-7 |
| FR-5 Guess submission | client/src/hooks/useGame.ts → server/src/routes/guess.ts | AD-5, AD-6 |
| FR-6 Invalid word rejection | server/src/validate.ts | AD-5 |
| FR-7 Feedback scoring | server/src/feedback.ts | AD-6 |
| FR-8 Win detection | client/src/hooks/useGame.ts | AD-7 |
| FR-9 Loss detection | client/src/hooks/useGame.ts | AD-7 |
| FR-10 Daily word selection | server/src/daily-word.ts | AD-2 |
| FR-11 Play Again word selection | server/src/routes/play-again.ts | AD-8 |
| FR-12 Player skill tracking | client/src/hooks/useStats.ts | AD-7 |
| FR-13 Results display | client/src/screens/Screen3.tsx | AD-7 |
| FR-14 Statistics display | client/src/screens/Screen3.tsx | AD-7 |
| FR-15 Play Again button | client/src/screens/Screen3.tsx | AD-7 |
| FR-16 Multi-algorithm solver | solver/src/ | Paradigm (offline script) |
| FR-17 Word ranking output | solver/src/ → data/word-bank.json | AD-4 |
| FR-18 Word bank curation | data/word-bank.json (generated) | AD-4 |
| FR-19 Browser persistence | client/src/hooks/usePersistence.ts | AD-7 |

## Deferred

- **Deployment mechanism** — how the server process is started, restarted, proxied (nginx, systemd). Owned by the deployment environment, not the architecture.
- **Solver algorithm selection** — which two or more specific algorithms (entropy, minimax, frequency-based). Deferred to implementation; the solver package interface is the invariant.
- **Skill metric formula** — exact calculation (simple average, weighted recent). Deferred to implementation; the API already carries a `skillMetric` number.
- **Word bank source** — which public-domain word list. Deferred to implementation; the `WordBank` interface is the invariant.
- **Hints on attempts 5-6** — post-v1. API already carries `tryNo`; the guess response shape is ready for a `hint` field.
- **HTTPS/TLS termination** — owned by the deployment environment (reverse proxy), not the application.
- **npm workspace configuration** — tooling detail, not an architectural invariant. The package boundary rules in AD-9 are sufficient.
- **Screen navigation/routing** — 3 screens, state-based switching (React state or reducer). No router library needed at this scale.
- **Feedback latency** — v1 accepts network round-trip per guess. Optimistic UI (show letters immediately, confirm colors) is a future enhancement.
- **Accessibility (color-blind mode, ARIA, screen reader)** — deferred to post-v1. Core game uses green/yellow color distinction; a color-blind accessible palette is a future enhancement.
- **Win celebration animation** — UI detail owned by the Results Screen component. No architectural constraint.
