# Architectural Spec: Landing Page, SteamDB Integration, API Security, Changelog **Date:** 2026-05-14 **Target Version:** 2026.0.101 **Status:** Draft — pending user review --- ## 1. Objective Overview Four independent workstreams bundled into one release: | # | Stream | Scope | |---|--------|-------| | A | **[Submit] SteamDB Version Auto-Fetch** | Auto-fetch latest game version from SteamDB and surface it in the submit wizard version selector | | B | **[Feature] Landing Page** | Add trending/hot/new/tested/reported sections below the hero, with a "peek" effect | | C | **[Infra] API Security & Rate Limiting** | Abuse prevention on submissions, anti-spam for comments, general hardening | | D | **[CHANGELOG]** | Bump version, write update news, update CHANGELOG.md | --- ## 2. Stream A — SteamDB Version Auto-Fetch ### 2.1 Current State - `gameVersions` table: `id`, `gameId`, `buildId`, `versionString`, `isLatest`, `createdAt` - Submit wizard (Setup step) shows a ``: ``` ⬇ Latest from SteamDB: v1.2.3 (Build 12345678) — recommended ``` 3. Selecting this option triggers version creation with the SteamDB data pre-filled, or (if the build ID already matches an existing version) selects that existing version. 4. The option should be visually distinct (e.g., with a small SteamDB badge/icon) to distinguish it from user-entered versions. #### 2.2.5 Constraints - **No SteamDB scraping if `steamAppId` is null** (manual/GOG/Epic games) - **No automatic version creation** — the user must explicitly choose the SteamDB suggestion - **Respect SteamDB's rate limits** — do not fetch on every keystroke; fetch only on wizard mount or explicit refresh - **The scraping is best-effort** — SteamDB may change their HTML structure; graceful degradation is required ### 2.3 New Files | File | Purpose | |------|---------| | `lib/api/steamdb-version.ts` | New Elysia route for the endpoint | | `lib/steamdb/scrape.ts` | HTML scraping logic (fetch + parse) | | `lib/steamdb/cache.ts` | In-memory cache with TTL | ### 2.4 Modified Files | File | Change | |------|--------| | `lib/api/index.ts` | Export new `steamdbVersionRoutes` | | `lib/api/app.ts` | Register `steamdbVersionRoutes` | | `components/wizard/steps/setup-step.tsx` | Add SteamDB version fetch + special option | | `app/game/[id]/submit/page.tsx` | No changes needed (data fetched client-side) | --- ## 3. Stream B — Landing Page ### 3.1 Current State - `app/page.tsx` renders a full-height hero section with animated tagline and search bar - No game cards, no data sections - Server-route `/api/dashboard/trending`, `/api/dashboard/best-new-releases`, `/api/dashboard/most-tested`, `/api/dashboard/most-reported` already exist and return data ### 3.2 Design #### 3.2.1 Layout & "Peek" Effect ``` ┌─────────────────────────────────┐ │ │ │ HERO SECTION │ height: calc(100svh - 10svh) │ (tagline, search, etc.) │ │ │ ├─────────────────────────────────┤ ← 10svh of next section peeks above fold │ ┌───────────────────────────┐ │ │ │ Trending This Week │ │ │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ │ │Game │ │Game │ │Game │ │ │ │ │ └─────┘ └─────┘ └─────┘ │ │ │ └───────────────────────────┘ │ │ ┌───────────────────────────┐ │ │ │ Best New Releases │ │ │ ... │ ``` **Implementation:** - Hero section gets `min-h-[calc(100svh-10svh)]` (formerly `h-[calc(100vh-3.6rem)]`) - The sections below start immediately, so ~10svh of the first card row is visible without scrolling - This REQUIRES the hero to not use `overflow: hidden` — let content naturally overflow #### 3.2.2 Section Order (top to bottom) 1. **Hero** (existing — keep as-is with height adjustment) 2. **Trending This Week** — from `/api/dashboard/trending` 3. **Best Performing New Releases** — from `/api/dashboard/best-new-releases` 4. **Most Tested Games** — from `/api/dashboard/most-tested` 5. **Most Reported Games** — from `/api/dashboard/most-reported` #### 3.2.3 Card Design Each section renders a horizontal scrollable row of compact game cards. Card design follows the **search result card** pattern from `app/search/page.tsx`: - **Cover image** (aspect 2:3, `w-20 sm:w-24 md:w-28`, rounded-lg) - **Title** (truncated, semibold) - **Playability badge** (from search card) - **Relevant stat** (varies by section): - Trending: activity score or benchmark count - Best New Releases: avg FPS - Most Tested: benchmark count - Most Reported: report count - **Hover/tap:** scales to 1.02, redirects to `/game/{id}?sync=1` #### 3.2.4 Data Fetching Strategy The landing page remains a **client-side component** (consistent with the current `"use client"` pattern and the search page): - Hero section keeps its animations and client state (search bar, rotating tagline) - Data sections fetch from the existing public dashboard endpoints on mount via `useEffect` + `fetch`: - `GET /api/dashboard/trending` - `GET /api/dashboard/best-new-releases` - `GET /api/dashboard/most-tested` - `GET /api/dashboard/most-reported` - Each section shows a **skeleton loader** (pulsing placeholder cards) while fetching - All 4 fetches fire in parallel via `Promise.all` - The page does NOT need to be a Server Component — the dashboard queries are fast, public, and the hero already requires client-side interactivity #### 3.2.5 Empty States - If a section returns 0 results, hide the section entirely (don't show "No trending games") - This avoids a dead page for new/quiet periods #### 3.2.6 Styling Constraints - Follow existing Tailwind theme variables (`--color-background`, `--color-primary`, `--color-text`, `--color-border`, etc.) - Section headers: `text-sm font-semibold text-text/80` with a subtle left border accent (`border-l-2 border-primary pl-3`) - Horizontal scroll: use `overflow-x-auto` with `scrollbar-hide` or custom thin scrollbar - Cards: replicate the `bg-text/3 border border-border rounded-xl` pattern from search cards - Use `motion` (framer-motion fork) for stagger animations on card reveal - Maintain 44px touch targets for interactive elements ### 3.3 New/Modified Files | File | Change | |------|--------| | `app/page.tsx` | Major refactor — add data sections below hero, adjust hero height | | `components/landing/` | New directory with section components (or inline in page.tsx for MVP) | --- ## 4. Stream C — API Security & Rate Limiting ### 4.1 Current State - **Rate limiter:** In-memory, global 60s window, 100 requests per IP+path (`lib/auth/rate-limit.ts`) - **Auth guards:** Role-based (`requireAuth`, `requireRole`, `requireAdmin`, `requireContributorOrAdmin`) - **Comments:** Auth required, soft-delete, no duplicate/spam detection - **Submissions:** Auth required, no per-user rate limits - **Reports:** One per user per entry (DB unique constraint) - **Community suggestions:** One per user per game per field (DB unique constraint) ### 4.2 Identified Gaps | Gap | Risk | Severity | |-----|------|----------| | No per-route rate limits | A logged-in user can POST comments as fast as they can send requests | Medium | | No duplicate comment detection | Identical content can be posted repeatedly | Low-Medium | | No submission flood protection | A user can submit many benchmarks in quick succession | Medium | | Global rate limit is coarse | All routes share one 100 req/min bucket; legitimate burst traffic may be blocked | Low | | No content length validation on comments | Extremely long comments could be submitted | Low | | No input sanitization beyond Elysia validation | Tiptap JSON is stored as-is; XSS risk if renderer is flawed | Low | | No CSRF protection on state-changing endpoints | Token-based auth mitigates this partially | Low | | In-memory rate limiter doesn't scale | Multi-instance deployments would have separate counters | Low (current deploy is single-instance) | ### 4.3 Design #### 4.3.1 Tiered Rate Limiting Replace the single global rate limiter with **route-category limits**: | Category | Window | Max Requests | Applies To | |----------|--------|-------------|------------| | `default` | 60s | 100 | All unlisted routes | | `auth` | 60s | 20 | Login, OTP, passkey endpoints | | `read` | 60s | 300 | GET endpoints (search, listing, game details) | | `write` | 60s | 10 | POST comment, POST submission, POST report | | `strict` | 60s | 5 | POST contact form, community suggestion | **Implementation:** - Extend `rateLimit` to accept an optional `category` parameter - Apply different limits per route group in `app.ts` - Maintain the existing in-memory store pattern (no Redis dependency for now) #### 4.3.2 Comment Anti-Spam 1. **Duplicate detection (exact match):** - Before inserting a comment, check if the user has posted a comment with identical `content` (JSON stringified) to the same game in the last 5 minutes - If yes, return `409 Conflict` with `{ error: "Duplicate comment detected" }` 2. **Content length cap:** - Validate that `JSON.stringify(body.content).length <= 50000` (50KB) - Return `413 Payload Too Large` if exceeded 3. **Rate limit on comment creation:** - Covered by the `write` tier (10 req/min per IP) - Additional per-user limit: max 30 comments per hour across all games (DB query check) 4. **Ghost-ban pattern (future consideration):** - Add `isShadowBanned` boolean to users table (admin-only) - Shadow-banned users' comments appear to themselves but are hidden from others - **Scope:** Out of scope for this release; note as future enhancement #### 4.3.3 Submission Abuse Prevention 1. **Per-user submission cooldown:** - Before inserting a performance entry, check when the user last submitted ANY entry - If < 60 seconds ago, return `429 Too Many Requests` with `{ error: "Please wait before submitting another benchmark", retryAfter: N }` - This is a lightweight DB query: `SELECT MAX(createdAt) FROM performance_entries WHERE userId = ?` 2. **Duplicate entry detection:** - Check for existing entry with same `userId`, `versionId`, `hardwareSlug`, `upscalerType`, `frameGenMethod` within last 10 minutes - If found, warn but don't block (user may be re-submitting with corrections) 3. **Validation hardening:** - `fpsAvg`: must be between 1 and 500 - `fpsLow`/`fpsHigh`/`fpsOnePercentLow`: if provided, must be between 0 and 500 - `tdpWatts`: if provided, must be between 1 and 100 - `settingsJson`: maximum 20 categories, each with maximum 50 settings - `userNotes`: maximum 5000 characters #### 4.3.4 Additional Hardening 1. **Input sanitization on comment content:** - Strip `