12 KiB
Architectural Spec: Sync Unification, Nav Cleanup & PWA
Date: 2025-05-05 Status: Draft — Awaiting Tactical Planning Author: Autonomous Architect
1. Problem Statement
Three distinct objectives converge on a single architectural concern: data integrity through pipeline unification. The remaining objectives (nav removal, PWA) are scoped-down implementation tasks that share no domain coupling with the sync work.
1.1 Observed Symptoms
| Symptom | Root Cause |
|---|---|
| Steam review scores missing after initial game insert | Two separate code paths insert games; only one fetches review data |
Review data appears only after manual sync from /manage |
Manual sync uses the complete syncSteamGame pipeline; initial insert does not |
| Playability status not recalculated on initial insert | recalculatePlayability is called only in syncSteamGame, not in stub creation |
1.2 Three Divergent Sync Paths
Path A — Initial Insert (two locations):
lib/api/game-stub.ts→/games/stub(Elysia API route)app/game/[id]/page.tsx→createGameStub()(Next.js server component)- Fetches Steam API directly, inserts game row, returns
- Missing:
steamReviewScore,steamReviewSentiment,steamReviewCount,recalculatePlayabilitycall, error tracking fields (syncRetryCount,syncNextRetry,syncError)
Path B — Stale-While-Revalidate Auto-Sync:
app/game/[id]/page.tsx→ callssyncSteamGame()app/games/page.tsx→ callssyncSteamGame()- Uses full pipeline: Steam API + reviews + playability + error tracking
Path C — Manual Dashboard Sync:
lib/api/games.ts→gameSyncRoutes→POST /games/:gameId/sync- Uses same
syncSteamGame()as Path B
Conclusion: Paths B and C are identical. Path A diverges entirely.
2. Architecture Boundaries
2.1 Source of Truth
syncSteamGame(steamAppId: number) in lib/steam/sync.ts is the single canonical sync pipeline. Every code path that creates or updates a Steam game's data MUST route through this function.
2.2 Forbidden Patterns
- No direct
fetch("https://store.steampowered.com/api/appdetails/...")calls outside oflib/steam/sync.ts - No manual
db.insert(games)ordb.update(games)for Steam-sourced games that populates fields covered bysyncSteamGame - No duplicate field-mapping logic between Steam API response and DB schema
2.3 Allowed Extensions
syncSteamGamemay be extended with new fields (e.g., Steam Deck verification status)- Wrapper functions may call
syncSteamGamewith pre/post hooks (e.g., batch sync with progress reporting) - The stub-creation flow may insert a minimal stub (appId + title only) and then delegate to
syncSteamGamefor full population
3. Sync Unification Design
3.1 Two-Stage Insert Pattern
For initial game creation, use a stub-then-sync pattern:
User provides steamAppId
│
▼
┌─────────────────────────────┐
│ 1. Check if exists │
│ (by steamAppId) │
│ ├─ Exists → return │
│ └─ New → continue │
├─────────────────────────────┤
│ 2. Insert MINIMAL stub │
│ Fields: steamAppId, │
│ source="steam", │
│ title (placeholder), │
│ syncStatus="pending" │
├─────────────────────────────┤
│ 3. Call syncSteamGame() │
│ Populates ALL fields: │
│ title, developer, │
│ publisher, description, │
│ genres, images, prices, │
│ metacritic, reviews, │
│ platforms, requirements, │
│ categories, releaseDate, │
│ lastSync, syncStatus, │
│ error tracking │
├─────────────────────────────┤
│ 4. recalculatePlayability() │
│ (called inside │
│ syncSteamGame) │
├─────────────────────────────┤
│ 5. Return complete game │
└─────────────────────────────┘
3.2 Affected Files
| File | Change |
|---|---|
lib/api/game-stub.ts |
Replace direct Steam fetch + insert with stub insert → syncSteamGame call |
app/game/[id]/page.tsx createGameStub() |
Same refactor; delegate to syncSteamGame after minimal stub insert |
lib/steam/sync.ts syncSteamGame() |
Verify it handles the case where a game row exists but has only stub fields (idempotent update) — current implementation uses db.update() so it already requires the row to exist. This is correct for the two-stage pattern. |
3.3 Error Semantics
- Stub insert failure: Return 500 immediately — database is unreachable
- Sync failure after stub exists: The stub persists with
syncStatus="error"andsyncErrorpopulated. The game page renders with partial data. Retry is governed by exponential backoff inrecordSyncFailure. - Type rejection (e.g., DLC, soundtrack): The stub should not be inserted. Validate type before inserting the stub, or insert the stub and let
syncSteamGamemark it as errored (prefer validating upfront to avoid dead rows).
3.4 Validation Checklist
After unification, verify:
- New game insert via search:
steamReviewScoreis populated immediately - New game insert via direct URL:
steamReviewScoreis populated immediately - Existing stale game visited after 7+ days: auto-sync refreshes all fields
- Manual sync from
/manage: no regression (already usessyncSteamGame) - Non-game Steam IDs (DLC, demos) are rejected before stub creation
- Playability is recalculated on every sync (insert or update)
4. Navbar: Remove "Updates"
4.1 Scope
Single-file change. No cascading impacts.
4.2 Affected Files
| File | Change |
|---|---|
lib/routes.ts |
Remove the { title: "Updates", href: "/updates" } entry from the routes array |
4.3 Considerations
- The
/updatespage directory (app/updates/) may remain on disk (dead code). Can be removed in a separate cleanup pass. Priority: remove nav entry immediately. - No import references to check —
routesis consumed only bycomponents/navbar.tsxand the mobile sidebar, both of which iterateroutesdynamically.
5. PWA & Steam Deck UX
5.1 Current State
- ✅ Web manifest exists (
app/manifest.ts) withdisplay: "standalone", theme color, icons - ❌ No service worker registered — zero offline capability
- ❌ No touch-optimized interactions (hover-dependent UI elements)
- ❌ No gamepad navigation support
- ❌ No
viewportmeta withuser-scalable=nofor installed PWA feel - ✅
next.config.tshas remote image patterns configured (Steam CDN, SteamGridDB)
5.2 Design Decisions (Deferred to Tactical Planning)
These require explicit user input — the following are constraints, not implementation details:
5.2.1 Offline Strategy
Constraint: Must use Workbox or next-pwa for service worker generation. The SW must:
- Precache the app shell (layout, navbar, CSS, fonts)
- Cache game pages on first visit (stale-while-revalidate for HTML, cache-first for images from Steam CDN)
- Cache API responses from the Elysia backend for a short TTL (5 minutes for listings, 1 hour for game details)
- Provide a "You're offline" fallback UI when uncached pages are requested
Open question: Should offline mode show a curated "previously viewed" list, or a generic offline message?
5.2.2 Touch Optimization
Constraint: All interactive elements must meet a minimum 44×44px touch target (WCAG 2.1 AA). Specific areas:
- Navbar links and hamburger menu
- Game card tap targets (currently
Linkwrapping entire card — verify touch area) - Filter controls on
/games(chips, dropdowns) - Comment submission buttons
- Profile/settings forms
Open question: Should touch optimization include a dedicated "Deck Mode" layout toggle, or be applied universally as responsive CSS?
5.2.3 Gamepad Navigation
Constraint: Use the Gamepad API (navigator.getGamepads()). Implementation must:
- Map D-pad/left stick to focus navigation (roving tabindex)
- Map A button to
click()on focused element - Map B button to browser back
- Map L1/R1 (bumpers) to tab switching on game pages
- Provide a visual focus ring distinct from
:focus-visibleso mouse users aren't affected - Only activate when a gamepad input is detected (not on page load)
- Disable when mouse/keyboard input is detected (reclaim interaction)
Open question: Should gamepad support be a global utility hook or scoped to the game detail page only?
5.3 PWA Technical Boundaries
- Service worker must NOT cache authenticated pages (profile, manage) — these require live data
- Service worker must NOT cache POST/PATCH/DELETE API responses
- The
manifest.tsmust be updated with proper icon sizes (192px maskable, 512px) - A
viewportmeta tag must be set inlayout.tsx:content="width=device-width, initial-scale=1, viewport-fit=cover"
6. Security & Data Integrity Constraints
6.1 Steam API Keys
- The Steam Web API key (for reviews) is called server-side only in
syncSteamGame. No client-side exposure risk. - The SteamGridDB API key is also server-side only. No change needed.
6.2 Rate Limiting
syncSteamGamealready has 15s timeout on Steam API calls- Batch sync in
/managehas 1.5s inter-request delay - The background sync in
games/page.tsxalso has 1.5s delay - Constraint: The stub-then-sync pattern must not introduce new burst traffic. Single-game insert is inherently rate-limit-safe (one request per user action).
6.3 Database Integrity
steamAppIdhas a UNIQUE constraint — prevents duplicate stubssyncSteamGameusesdb.update().where(eq(games.steamAppId, ...))— safe for re-entry- Risk: If two requests race to create a stub for the same appId, one will fail on UNIQUE constraint. The loser must gracefully return the existing game.
7. Scope Boundaries
In Scope
- Unify initial insert to use
syncSteamGamepipeline (2 code locations) - Remove "Updates" from
lib/routes.ts - PWA architecture constraints and implementation boundaries (spec only — implementation is a separate planning cycle)
Out of Scope
- Refactoring
syncSteamGameinternals (already correct) - Removing
/app/updates/directory (cosmetic cleanup, deferred) - Adding new Steam API fields beyond what
syncSteamGamealready fetches - Offline support for
/manageor authenticated routes - Native mobile app packaging (PWA only)
8. Risk Register
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Stub-then-sync pattern doubles API calls for new games | Certain | Low (2 calls instead of 1, 1.5KB payload each) | Acceptable trade-off for data integrity |
| Race condition: two simultaneous stub creations | Low | Low (one returns 409, can be caught) | UNIQUE constraint on steamAppId handles this |
| Service worker caches stale game data | Medium | Medium (user sees old reviews/prices) | Stale-while-revalidate strategy + short TTL for game pages |
| Gamepad API not available on all Steam Deck browser versions | Low | High (feature non-functional) | Feature-detect and silently degrade; SteamOS 3.5+ ships Chromium 114+ with Gamepad API support |
9. Dependencies
- Sync unification: No external dependencies. Pure refactor of existing code.
- Nav removal: No dependencies.
- PWA: Requires
next-pwaor@serwist/nextpackage, plusworkbox-webpack-pluginconfiguration. These are new production dependencies that must be vetted for Next.js 15+ compatibility.