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']
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.
[ADOPTED](daysSinceEpoch(EST/EDT) mod targetWordCount) + 1. Same date always maps to same word for every player. Timezone: America/New_York. [ADOPTED]shared/ are the single source of truth for API contracts, word bank shape, and game types. [ADOPTED]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]{ 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][ADOPTED]tryNo the client sends. [ADOPTED][ADOPTED]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]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]'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]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]| 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 |
| Name | Version |
|---|---|
| TypeScript | ^6.x |
| Node.js | ^24 LTS |
| React | ^19.x |
| Vite | ^8.x |
| Express | ^5.x |
shared/ types package |
workspace |
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]
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 / 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 |
skillMetric number.WordBank interface is the invariant.tryNo; the guess response shape is ready for a hint field.