# KnowFoolery MVP Implementation Plan ## Context KnowFoolery is a trivia quiz game. The repo currently contains only documentation (requirements + architecture specs) and no source code. The goal is to implement an MVP that is playable end-to-end, while establishing a project structure that accommodates future evolution: sophisticated auth (OAuth), observability, caching, production database, and microservice extraction. Tech stack: Rust/Axum/sqlx/SQLite backend, SolidJS/TypeScript/SUID frontend, Docker Compose for dev. **MVP scope includes**: email/password authentication, full game flow, leaderboard, and basic admin API for question/user management. --- ## Project Structure ``` knowfoolery.rust/ ├── Cargo.toml # Workspace root ├── .gitignore ├── .env.example ├── docker-compose.yml ├── Dockerfile.backend ├── Dockerfile.frontend ├── README.md │ ├── crates/ │ ├── knowfoolery-shared/ # Domain models, errors, validation, fuzzy matching │ │ ├── Cargo.toml # Deps: serde, thiserror, uuid, strsim, chrono │ │ └── src/ │ │ ├── lib.rs │ │ ├── models/ # Player, Question, Session, SessionQuestion, LeaderboardEntry │ │ ├── error.rs # AppError enum (thiserror) │ │ ├── validation.rs # Input validation (player name, email, answer text) │ │ └── fuzzy.rs # Fuzzy answer matching (strsim, 85% threshold) │ │ │ └── knowfoolery-server/ # Axum binary │ ├── Cargo.toml │ ├── migrations/ # sqlx migrations (001-006) │ └── src/ │ ├── main.rs # Bootstrap: env, tracing, db pool, migrations, serve │ ├── config.rs # Env-based config (DATABASE_URL, PORT, JWT_SECRET, etc.) │ ├── app_state.rs # AppState: db pool, config, services │ ├── startup.rs # Router construction + middleware stack │ ├── auth/ # Auth module │ │ ├── mod.rs │ │ ├── password.rs # argon2 password hashing/verification │ │ └── jwt.rs # JWT token creation/validation (simple HMAC-SHA256) │ ├── repositories/ # Trait definitions + sqlite/ implementations │ │ ├── mod.rs │ │ ├── traits.rs # All repository trait definitions │ │ └── sqlite/ # SQLite implementations │ ├── services/ # Business logic │ │ ├── mod.rs │ │ ├── auth_service.rs # Register, login, token validation │ │ ├── game_service.rs # Core game logic │ │ └── admin_service.rs # Question/user CRUD │ ├── handlers/ # Thin Axum handlers │ │ ├── mod.rs │ │ ├── auth.rs # POST /register, POST /login │ │ ├── game.rs # Game play endpoints │ │ ├── session.rs # Session management │ │ ├── leaderboard.rs # Leaderboard │ │ ├── admin.rs # Admin CRUD endpoints │ │ └── health.rs # Health check │ ├── middleware/ # Tower middleware │ │ ├── mod.rs │ │ └── auth.rs # JWT extraction + role-based guard │ └── dto/ # Request/Response types │ ├── mod.rs │ ├── requests.rs │ └── responses.rs │ ├── frontend/ │ ├── package.json │ ├── vite.config.ts │ └── src/ │ ├── App.tsx │ ├── api/ # Typed fetch wrapper + endpoint modules │ │ ├── client.ts # Base fetch with JWT header injection │ │ ├── auth.ts # Register/login calls │ │ ├── game.ts │ │ ├── session.ts │ │ ├── leaderboard.ts │ │ └── admin.ts │ ├── components/ # GameBoard, QuestionCard, AnswerInput, Timer, Leaderboard, etc. │ ├── pages/ # Login, Register, Home, Game, Results, Leaderboard, Admin │ ├── stores/ # SolidJS reactive stores (auth, game, player) │ ├── types/ # TypeScript types mirroring backend DTOs │ └── styles/ # SUID theme │ └── scripts/ └── seed.sql ``` ### Why this structure - **Two-crate workspace**: `knowfoolery-shared` has zero framework dependencies, compiles fast, and can be shared by future binaries (admin service, workers). Adding a new microservice = adding a new binary crate that depends on shared. - **Repository trait pattern**: All DB access behind `async_trait` interfaces. Future: add `postgres/` or caching decorator wrapping same traits. Zero changes to service code. - **Layered middleware stack**: `startup.rs` composes Tower layers. Adding observability/rate-limiting = adding a line. --- ## Database Schema 6 migrations using sqlx's built-in migration system. **001 - players** ```sql id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, -- display name, 2-50 chars password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'player', -- 'player' | 'admin' created_at TEXT NOT NULL DEFAULT (datetime('now')) ``` **002 - questions** ```sql id TEXT PRIMARY KEY, theme TEXT NOT NULL, text TEXT NOT NULL, answer TEXT NOT NULL, hint TEXT, -- optional difficulty TEXT, -- future: 'easy'|'medium'|'hard' created_at TEXT NOT NULL DEFAULT (datetime('now')) ``` **003 - sessions** ```sql id TEXT PRIMARY KEY, player_id TEXT NOT NULL REFERENCES players(id), status TEXT NOT NULL DEFAULT 'active', -- active|completed|timed_out|abandoned score INTEGER NOT NULL DEFAULT 0, questions_answered INTEGER NOT NULL DEFAULT 0, correct_answers INTEGER NOT NULL DEFAULT 0, started_at TEXT NOT NULL DEFAULT (datetime('now')), ended_at TEXT -- Partial unique index: (player_id) WHERE status='active' ``` **004 - session_questions** ```sql id TEXT PRIMARY KEY, session_id TEXT NOT NULL REFERENCES sessions(id), question_id TEXT NOT NULL REFERENCES questions(id), attempts_used INTEGER NOT NULL DEFAULT 0, hint_used INTEGER NOT NULL DEFAULT 0, points_earned INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', -- active|correct|failed presented_at TEXT NOT NULL DEFAULT (datetime('now')), answered_at TEXT -- Unique index: (session_id, question_id) ``` **005 - answer_attempts** ```sql id TEXT PRIMARY KEY, session_question_id TEXT NOT NULL REFERENCES session_questions(id), submitted_answer TEXT NOT NULL, is_correct INTEGER NOT NULL, similarity_score REAL, submitted_at TEXT NOT NULL DEFAULT (datetime('now')) ``` **006 - seed_questions**: 30-50 trivia questions across themes (Geography, History, Science, Literature, Pop Culture). UUIDs stored as TEXT (portable to PostgreSQL). Timestamps as ISO 8601 TEXT. --- ## API Endpoints Base: `/api/v1`. All error responses: `{ "error": { "code", "message" } }`. ### Public (no auth) | Method | Path | Description | |--------|------|-------------| | GET | `/health` | Health check | | POST | `/auth/register` | Register `{ email, name, password }` → `{ token, player }` | | POST | `/auth/login` | Login `{ email, password }` → `{ token, player }` | | GET | `/leaderboard` | Top 10 scores | ### Authenticated (JWT Bearer token) | Method | Path | Description | |--------|------|-------------| | POST | `/sessions` | Start game session → session info + first question | | GET | `/sessions/:id` | Get session status, score, time remaining | | POST | `/sessions/:id/end` | End session voluntarily | | GET | `/sessions/:id/question` | Get current question | | POST | `/sessions/:id/question/:qid/answer` | Submit answer `{ "answer" }` | | POST | `/sessions/:id/question/:qid/hint` | Request hint | | POST | `/sessions/:id/question/next` | Next question or game-over | ### Admin (JWT + role=admin) | Method | Path | Description | |--------|------|-------------| | GET | `/admin/questions` | List all questions (paginated) | | POST | `/admin/questions` | Create question | | PUT | `/admin/questions/:id` | Update question | | DELETE | `/admin/questions/:id` | Delete question | | GET | `/admin/players` | List all players | | PUT | `/admin/players/:id/role` | Change player role | --- ## Key Design Decisions **Trait objects for repositories** (`Arc`): simpler than generics, dynamic dispatch cost negligible vs DB I/O, fully swappable. **JWT auth (HMAC-SHA256)**: Simple stateless tokens using `jsonwebtoken` crate. Token contains `{ player_id, role, exp }`. Middleware extracts and validates on every authenticated request. Future: swap for OAuth/OIDC. **Password hashing**: `argon2` crate with default params. Hashing happens in `auth/password.rs`, isolated for easy swapping. **Lazy session expiry**: No background timer. Every request checks `now - started_at >= 30min` and auto-transitions to `timed_out`. Simpler, sufficient for MVP. **Fuzzy matching**: `strsim` crate (Jaro-Winkler), normalize to lowercase/trimmed, exact match fast-path. Threshold 0.85. **Error handling**: Single `AppError` enum (`thiserror`) in shared crate. `IntoResponse` impl in server maps to HTTP status codes. Internal errors logged but sanitized. **Performance / low memory**: sqlx compile-time checked queries, connection pool (10 for SQLite), `serde` serialization, Tokio async runtime. Lean domain structs. --- ## Implementation Phases ### Phase 1: Scaffolding - Cargo workspace with both crates (stub `lib.rs` and `main.rs`) - SolidJS project init (add SUID, configure Vite proxy to backend) - `.gitignore`, `.env.example` (DATABASE_URL, PORT, JWT_SECRET, RUST_LOG) - `/health` endpoint running - **Verify**: `cargo build` + `npm run dev` both succeed ### Phase 2: Database + Repositories - Write 6 SQL migrations - Domain models in `knowfoolery-shared/src/models/` - `AppError` enum in `knowfoolery-shared/src/error.rs` - Repository traits in `repositories/traits.rs`: PlayerRepo, QuestionRepo, SessionRepo, SessionQuestionRepo, LeaderboardRepo - SQLite implementations in `repositories/sqlite/` - `AppState` with `SqlitePool` - **Verify**: Integration tests against in-memory SQLite exercising all repository methods ### Phase 3: Auth - `auth/password.rs`: hash and verify with argon2 - `auth/jwt.rs`: create and validate tokens (jsonwebtoken crate, HMAC-SHA256) - `AuthService`: register (validate email/name/password, hash password, create player, return token), login (verify credentials, return token) - `middleware/auth.rs`: extract Bearer token, validate JWT, inject `AuthContext { player_id, role }` into request extensions. Role-based guard for admin routes. - Auth handlers: `POST /auth/register`, `POST /auth/login` - Validation: email format, password min 8 chars, name 2-50 chars alphanumeric+spaces - **Verify**: Register, login, access protected endpoint with token, reject without token ### Phase 4: Game Logic + Services - `fuzzy.rs` with `strsim` (Jaro-Winkler, threshold 0.85) - `validation.rs` (answer non-empty max 200 chars) - `GameService`: create_session, get_current_question, submit_answer, request_hint, next_question, end_session, get_leaderboard - Lazy session expiry in every session-touching method - **Verify**: Unit tests with mock repositories. Cover: scoring, attempts, hints, expiry, fuzzy edge cases ### Phase 5: HTTP Handlers + Routing - Request/Response DTOs (`#[serde(rename_all = "camelCase")]`) - Game + session + leaderboard handlers - Admin handlers: CRUD questions, list players, change roles - Router in `startup.rs`: public routes, authenticated routes (with auth middleware), admin routes (with auth + role guard) - `IntoResponse` for `AppError` - `main.rs` full bootstrap - **Verify**: Full game flow via curl. Admin CRUD via curl with admin token. ### Phase 6: SolidJS Frontend - SUID theme + layout - API client with JWT injection (store token in memory, attach as Bearer header) - Auth store + pages: Login, Register (forms with validation) - Game stores + pages: Home (start game), Game (timer + question + answer + hint), Results - Leaderboard page - Admin page: question list with create/edit/delete, player list with role management - Protected routes (redirect to login if no token) - Anti-cheat client measures: paste-disabled on answer input - **Verify**: Full flow in browser — register → login → play game → results → leaderboard. Admin flow. ### Phase 7: Docker + Dev Experience - `Dockerfile.backend` (multi-stage with cargo-chef for layer caching) - `Dockerfile.frontend` (Node slim, Vite dev server) - `docker-compose.yml` (backend + frontend, volumes for hot reload) - **Verify**: `docker compose up` starts both, full game playable --- ## How Future Evolution is Accommodated | Future Need | Structural Hook | |---|---| | OAuth/OIDC auth | Replace JWT creation in `auth/jwt.rs`, add OAuth flow in `auth_service.rs`. Middleware already extracts claims. | | PostgreSQL | New `repositories/postgres/` module implementing same traits. Toggle via `DATABASE_URL`. | | Redis caching | Decorator: `CachingQuestionRepo` wrapping `Arc`. | | OpenTelemetry | Replace `tracing_subscriber::fmt` with OTel pipeline. Existing `tracing` calls auto-become spans. | | Rate limiting | Add Tower layer in `startup.rs`. | | Microservice extraction | New binary crate in workspace depending on `knowfoolery-shared`. | | Production config | `config.rs` reads from env — different values per environment. |