You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

14 KiB

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

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

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

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

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

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<dyn PlayerRepository>): 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<dyn QuestionRepository>.
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.