Frontend Architecture
Purpose and Constraints
Section titled “Purpose and Constraints”This page is the source of truth for frontend architecture in frontend-pwa/.
- We build a single-page PWA with Svelte 5 runes.
- We use global rune stores in
src/lib/*.svelte.tsfor app state. - We host static game assets locally (for example, piece SVGs in
public/pieces/) to keep gameplay views available offline. - We do not use legacy Svelte store patterns (
writable,derived, customsubscribestores). - Components are view composition + interaction wiring, not data orchestration layers.
Runtime Topology
Section titled “Runtime Topology”src/App.svelte is the shell and route-like state switcher.
- It imports global stores (
authStore,gameListStore,activeGameStore,modeStore, etc.). - It performs startup orchestration in
onMount():- Parse URL query params.
- Load shared game state when
gameis present. - Load isolated shared puzzle mode when
puzzleis present. - Fetch authenticated user via
authStore.fetchMe(). - Trigger initial game list fetch when user is authenticated and approved.
- It uses
$effectfor shell-level side effects (for example, syncing URL query params with current game/move).
State Management Pattern (Svelte 5 Runes)
Section titled “State Management Pattern (Svelte 5 Runes)”Store module structure
Section titled “Store module structure”Use the centralized logger utility in src/lib/utils/logger.ts instead of ad-hoc
console.log statements.
Logger Quick Reference
Section titled “Logger Quick Reference”debug(message, payload?)— development-only diagnosticsinfo(message, payload?)— development-only operational eventswarn(message, payload?)— always logged for recoverable problems or degraded stateserror(message, payload?)— always logged for failures
debug and info are suppressed when import.meta.env.DEV is false. warn and
error stay enabled in all environments.
- Set
isLoading/loading...before request. - Reset
error/...Errorbefore request. - Call API through
ApiClientonly. - On success, update state atomically.
- On failure, store user-facing error message (plus optional
console.errorfor debugging). - Always clear loading flags in
finally.
This pattern is visible in authStore.fetchMe(), gameListStore.fetchGames(), and adminStore.fetchUsers().
The logger accepts primitive values, objects, and Error instances. When an Error is
passed as payload, it is formatted to preserve name, message, and stack data.
All HTTP goes through ApiClient.
Base request behavior
Section titled “Base request behavior”ApiClient.request<T>() provides shared behavior:
credentials: 'include'for cookie-based auth.- JSON headers by default.
- Uniform error translation to
ApiErrorwithstatusand message. Store actions should use the logger for diagnostics around async work and state changes:- Non-JSON body -> text
Typed convenience methods
Section titled “Typed convenience methods”ApiClient exposes both generic verbs and domain helpers:
- Generic:
get,post,delete,request. - Domain-specific wrappers:
fetchPendingUsers,approveUser,triggerGameSync,getBookmarks, etc. - Domain-specific wrappers:
fetchUsers,updateUser,triggerGameSync,getBookmarks,getNextPuzzle, etc.
Stores should call these wrappers instead of duplicating endpoint URLs and request options.
Error and optimistic-update handling
Section titled “Error and optimistic-update handling”- Optimistic UI updates are allowed in stores (e.g.,
gameListStore.toggleFavorite). - Any optimistic mutation must have explicit rollback behavior on API failure.
- Error messages propagated from
ApiErrorshould be user-safe and concise.
Testing Strategy (Vitest-First for Store Logic)
Section titled “Testing Strategy (Vitest-First for Store Logic)”Store logic is the primary test target.
Tooling
Section titled “Tooling”- Test config:
frontend-pwa/vitest.config.ts - Environment:
jsdom - Globals enabled (
describe,it,expect, etc.)
What we test most
Section titled “What we test most”We prioritize deterministic unit tests for store behavior over UI/E2E coverage:
- State transitions for loading/error/success paths.
- Query/parameter construction for API requests.
- Pagination and filter state behavior.
- Derived field behavior (
totalPages,pendingUsers, turn boundaries, player names, board orientation). - Async flow with timers where needed (e.g., playback in
activeGameStorewith fake timers). - Optimistic update + rollback behavior.
Examples:
src/lib/gameListStore.test.tssrc/lib/activeGameStore.test.tssrc/lib/adminStore.test.tssrc/lib/bookmarksStore.test.ts
Why this bias exists
Section titled “Why this bias exists”- Most business logic is in stores, not components.
- Store tests run fast and isolate data/state regressions.
- UI/E2E tests are useful but secondary for this codebase and should focus on critical flows only.
Logging Strategy
Section titled “Logging Strategy”Use the centralized logger utility in src/lib/utils/logger.ts instead of ad-hoc
console.log statements.
Logger Quick Reference
Section titled “Logger Quick Reference”debug(message, payload?)— development-only diagnosticsinfo(message, payload?)— development-only operational eventswarn(message, payload?)— always logged for recoverable problems or degraded stateserror(message, payload?)— always logged for failures
debug and info are suppressed when import.meta.env.DEV is false. warn and
error stay enabled in all environments.
import { logger } from '$lib/utils/logger';
logger.debug('Loading game...', { gameId: '123' });logger.info('Game data loaded successfully', gameData);logger.warn('Slow network detected', { latency: 2500 });logger.error('Failed to sync player data', error);The logger accepts primitive values, objects, and Error instances. When an Error is
passed as payload, it is formatted to preserve name, message, and stack data.
try { // ... operation} catch (error) { logger.error('Operation failed', error); // Stack trace preserved}Store actions should use the logger for diagnostics around async work and state changes:
export class GameListStore { async fetchGames(): Promise<void> { logger.debug('Fetching games...', { page: this.currentPage }); try { const games = await apiClient.getGames(); logger.info('Games loaded', { count: games.length }); this.games = games; } catch (error) { logger.error('Failed to fetch games', error); this.error = 'Unable to load games. Please try again.'; } }}Behavior is covered by src/lib/utils/logger.test.ts, including environment filtering,
payload handling, and error formatting.
Board Component
Section titled “Board Component”The interactive chessboard is rendered by ChessgroundBoard.svelte using the svelte5-chessground wrapper (v1.1.1).
Why svelte5-chessground
Section titled “Why svelte5-chessground”The wrapper provides a Svelte 5 Runes-native interface around the vanilla chessground library. It eliminates the need for manual onMount, ResizeObserver, and imperative cgApi.set() calls by accepting reactive Svelte props and handling all $effect-based updates internally.
Reactive Prop Convention
Section titled “Reactive Prop Convention”Board state is driven entirely by $derived values sourced from activeGameStore. Use the flat shorthands for simple cases:
<script lang="ts"> import { Chessground } from 'svelte5-chessground'; import 'svelte5-chessground/style.css';
const fen = $derived(activeGameStore.currentState?.fen); const orientation = $derived(activeGameStore.boardOrientation);</script>
<Chessground {fen} {orientation} />Use
fenandorientationprops for simple cases. For advanced configuration (custom destinations, drawable shapes, etc.) use theconfigprop — but do not mix the two for the same property, as shorthand props override theirconfigequivalents.
Dice Chess Configuration
Section titled “Dice Chess Configuration”Dice Chess permits King captures and moving into check. Disable all move validation:
<Chessground movableFree={true} movableColor={orientation} turnColor={orientation}/>movableFree={true}— disables piece-rule validation; any piece can go anywhere.movableColorandturnColorare kept in sync withorientationso chessground’s internalisMovable()check passes after every state update.
Do NOT
Section titled “Do NOT”- Instantiate the vanilla
Chessground()function directly — use the Svelte component. - Hold a manual
cgApireference for state updates; pass reactive props instead (reservebind:apionly for imperative operations like programmatic move animations).
Guardrails for Contributors and AI Agents
Section titled “Guardrails for Contributors and AI Agents”- Use Svelte 5 runes state in
.svelte.tsstores; do not introduce legacy Svelte store APIs. - Keep app data flow unidirectional: component event -> store action -> store state -> UI.
- Keep API calls centralized in
ApiClient. - Add or update Vitest store tests whenever store logic changes.
- Use the centralized
loggerutility instead of ad-hocconsole.logstatements.