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-sharedhas 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_traitinterfaces. Future: addpostgres/or caching decorator wrapping same traits. Zero changes to service code. - Layered middleware stack:
startup.rscomposes 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.rsandmain.rs) - SolidJS project init (add SUID, configure Vite proxy to backend)
.gitignore,.env.example(DATABASE_URL, PORT, JWT_SECRET, RUST_LOG)/healthendpoint running- Verify:
cargo build+npm run devboth succeed
Phase 2: Database + Repositories
- Write 6 SQL migrations
- Domain models in
knowfoolery-shared/src/models/ AppErrorenum inknowfoolery-shared/src/error.rs- Repository traits in
repositories/traits.rs: PlayerRepo, QuestionRepo, SessionRepo, SessionQuestionRepo, LeaderboardRepo - SQLite implementations in
repositories/sqlite/ AppStatewithSqlitePool- Verify: Integration tests against in-memory SQLite exercising all repository methods
Phase 3: Auth
auth/password.rs: hash and verify with argon2auth/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, injectAuthContext { 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.rswithstrsim(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) IntoResponseforAppErrormain.rsfull 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 upstarts 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. |