Multi-player trivia game with AI-generated questions. Players join a room using a 6-character code, answer timed questions across 6 categories, and compete for wedges to complete the board.
- Backend: FastAPI + SQLAlchemy + SQLite
- AI: LiteLLM proxy (set
LITELLM_BASEandLITELLM_API_KEYin.env— BYOK) for question generation - Scrapers: Reddit, Pinterest, Threads, Instagram, Facebook, Steam, Wikipedia, OSM, Wikidata, OpenLibrary, GDELT, crawl4ai
- Cache: Entity-cache layer (SQLite, persists across profiles)
- Container: Docker + docker-compose on phatt-RAID (Unraid)
# Local development
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
# Docker
docker-compose up -dA fresh clone ships with an empty database (0 questions, no consented profile), so hosting a game fails until something is seeded. To get playable in one command — fully offline, no LLM required:
python3 scripts/seed_demo.py # demo profile "Alex Delgado": consent granted + 25 questions
uvicorn app.main:app --reloadThen open http://localhost:8000, click New Game, pick Alex Delgado, and host — share the 6-character room code with players. Re-running the script is safe (it replaces the demo profile's questions).
app/
database.py — SQLite setup, all table models (Profile, Question, GameSession, Player, Answer, PlayerStats, EntityCache)
models.py — Pydantic request/response schemas
config.py — Settings via pydantic-settings
main.py — FastAPI app, static file serving
routes/
profiles.py — CRUD + scrape trigger + question generation
games.py — Game lifecycle, websocket, scoring
admin.py — Ops overview, cache management endpoints
services/
scraper/ — All content scrapers (reddit, wikipedia, osm, etc.)
entity_cache.py — Shared cache service
generator.py — Manual fact fallback question generation
Each scraper checks entity_cache before making HTTP calls. Cache miss → scrape → write cache. No expiration, no re-scrape policy.
Supported entity types: person, place, thing, event.
| Method | Path | Description |
|---|---|---|
| GET | /api/health |
Health check |
| GET/POST | /api/profiles |
Profile CRUD |
| POST | /api/profiles/{id}/scrape |
Trigger scrape + cache |
| POST | /api/profiles/{id}/generate |
Trigger question generation |
| GET/POST | /api/games |
Game management |
| POST | /api/games/{room_code}/join |
Join a game |
| WS | /ws/{room_code}/{player_id} |
WebSocket for live game events |
| GET | /api/games/{room_code}/question |
Get current question |
| GET | /api/admin/overview |
Ops stats |
| POST | /api/admin/cache/delete/all |
Clear entity cache |
| GET | /api/admin/cache/stats |
Cache statistics |
| POST | /api/admin/games/{room_code}/clear |
Delete a game session |
/api/admin/* includes destructive operations (cache wipe, profile re-scrape, game deletion). Access is gated by the ADMIN_TOKEN env var:
- Empty / unset (default): admin routes are open. Only safe behind LAN/VPN — anything that can reach the FastAPI port can wipe the cache or delete games.
- Set to a value: every
/api/admin/*request must sendAuthorization: Bearer <ADMIN_TOKEN>. Missing/invalid tokens get a401.
Generate a strong token and set it before exposing Obsessed to the public internet:
export ADMIN_TOKEN=$(openssl rand -hex 32)
curl -H "Authorization: Bearer $ADMIN_TOKEN" https://obsessed.example.com/api/admin/overview# Build and push to Docker Hub
docker build -t therealphatt/obsessed:latest .
docker push therealphatt/obsessed:latest
# Deploy via docker-compose
docker-compose up -d| Variable | Default | Description |
|---|---|---|
LITELLM_API_KEY |
— | API key for LiteLLM proxy (BYOK) |
STEAM_API_KEY |
— | Steam Web API key (free at steamcommunity.com/dev/apikey). Enables full library enrichment; without it only basic profile XML is fetched. |
STEAM_API_KEY |
— | Use a burner phattvip account — not Brandon's personal Steam account. 100k calls/day quota; ~5 per profile scrape. If you want per-user review HTML (/profiles/{sid}/recommended/), that requires a separate account with a known-reviewer fixture — file a follow-up issue. |
LITELLM_BASE |
http://localhost:4000 |
LiteLLM proxy base URL — point at any OpenAI-compatible endpoint |
CONTENT_MAX_CHARS |
200000 |
Max chars per scraped source |
DATABASE_URL |
SQLite data/trivia.db |
Database connection |
ADMIN_TOKEN |
(empty) | If set, all /api/admin/* routes require Authorization: Bearer <token>. If empty, admin endpoints are open (single-host LAN/VPN deployments only — do not expose Obsessed to the public internet without setting this). |
All /api/admin/* routes are open by default (when ADMIN_TOKEN is unset). If Obsessed is reachable from outside your LAN/VPN, set ADMIN_TOKEN before deploying.
To enable token auth:
echo "ADMIN_TOKEN=your-secret-token" >> .envAll admin requests must then include the header:
Authorization: Bearer <ADMIN_TOKEN>
Destructive endpoints (require token when set):
POST /api/admin/cache/delete/all— irreversibly wipe entity cachePOST /api/admin/cache/delete/by-date— wipe cache by date rangePOST /api/admin/profiles/{id}/rescrape— re-run full scraper chain for a profilePOST /api/admin/games/{room_code}/clear— delete a game session from DB
Read endpoints (also protected when token is set):
GET /api/admin/overview— ops stats snapshotGET /api/admin/profiles— all profile records with scrape statusGET /api/admin/leaderboard— player stats leaderboardGET /api/admin/games/recent— recently played gamesGET /api/admin/cache/stats— cache entry counts by type
Connect to /ws/{room_code}/{player_id} for real-time game events. The server broadcasts:
player_joined— new player entered the lobbygame_started— game moved from lobby to activenew_question— question text, options, timer, category badgeanswer_result— correct/incorrect with live player scoresquestion_advance— scores update between questionsgame_over— game finished, players see final results
Client sends {"type":"ping"} to keep connection alive. Auto-reconnect on disconnect (3s backoff).