ARCHITECTURE-SPINE.md 12 KB


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

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

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.