The 413 error on large file uploads was caused by nginx's default 1MB client_max_body_size. Increased to 100MB. The path step update UNIQUE constraint error occurred because ORM-level deletes weren't reliably flushed before re-inserts. Switched to a raw DELETE statement which executes immediately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Caccia al Tesoro v2
A real-time web application for playing "Find the Treasure" (Caccia al Tesoro) scavenger hunts. Admins create games with riddles, share a link + join code with players, and teams race to solve shuffled riddles.
Features
- Admin panel: create and manage riddles (text or multiple-choice, with optional images), create games, start/stop games, view live leaderboard and per-team/per-riddle analytics
- Player flow: join via link + code, form a team, wait for game start, solve riddles sequentially
- Shuffled riddle order: each team gets the same riddles in a different random order
- Real-time updates: WebSocket-powered waiting room, game start signal, and live leaderboard
- Anti-spam: server-enforced exponential backoff on wrong answers (5s, 10s, 20s, ... up to 120s cap)
- Analytics: tracks time spent per riddle, attempt counts, completion rates
Project structure
caccia_tesoro_v2/
├── backend/ Python (FastAPI) backend
│ ├── pyproject.toml Poetry project + dependencies
│ ├── app/
│ │ ├── main.py FastAPI app entry point
│ │ ├── config.py Settings (DB URL, JWT secret, etc.)
│ │ ├── database.py SQLAlchemy async engine + session
│ │ ├── dependencies.py Dependency injection (get_db, auth)
│ │ ├── models/ SQLAlchemy ORM models
│ │ │ ├── admin.py Admin users
│ │ │ ├── riddle.py Riddle definitions
│ │ │ ├── game.py Games
│ │ │ ├── game_riddle.py Game-riddle associations (with ordering)
│ │ │ ├── team.py Teams (with shuffled riddle order)
│ │ │ ├── team_member.py Team members
│ │ │ ├── submission.py Answer submissions
│ │ │ └── riddle_analytics.py Per-team per-riddle timing/attempt data
│ │ ├── schemas/ Pydantic request/response models
│ │ ├── routers/
│ │ │ ├── auth.py POST /api/auth/register, /api/auth/login
│ │ │ ├── riddles.py CRUD /api/riddles (admin)
│ │ │ ├── games.py CRUD /api/games + start/stop/leaderboard/analytics (admin)
│ │ │ ├── play.py /api/play/* player endpoints (join, riddle, submit)
│ │ │ └── ws.py WebSocket /api/ws/{slug}
│ │ └── services/
│ │ ├── auth.py Password hashing, JWT creation/verification
│ │ ├── game.py Game logic (shuffle, answer check, leaderboard)
│ │ └── ws_manager.py WebSocket connection manager
│ └── uploads/ Riddle images (gitignored)
├── frontend/ React (TypeScript) SPA
│ ├── package.json
│ ├── vite.config.ts Dev proxy to backend
│ └── src/
│ ├── App.tsx Router setup
│ ├── api/client.ts API fetch helpers
│ ├── context/
│ │ ├── AuthContext.tsx Admin JWT state
│ │ └── GameContext.tsx Player session state
│ ├── hooks/
│ │ └── useWebSocket.ts WebSocket hook with auto-reconnect
│ ├── pages/
│ │ ├── admin/ Login, Dashboard, Riddles CRUD, Game form, Live game view
│ │ └── player/ Join, Waiting room, Play, Results
│ ├── components/ Shared UI (Leaderboard, ProtectedRoute)
│ └── types/index.ts TypeScript interfaces
└── .gitignore
Prerequisites
- Python 3.11+
- Poetry 1.7+
- Node.js 18+
- npm
Setup
Backend
cd backend
poetry install
The database (SQLite) is created automatically on first startup. No migrations needed.
Frontend
cd frontend
npm install
Running (development)
Open two terminals:
Terminal 1 -- Backend (runs on port 8000):
cd backend
poetry run uvicorn app.main:app --reload --port 8000
Terminal 2 -- Frontend (runs on port 5173, proxies API to backend):
cd frontend
npm run dev
Open http://localhost:5173 in your browser.
Usage
1. Create an admin account
Go to http://localhost:5173/admin/login, click "Need an account? Register", and create an account.
2. Create riddles
Navigate to Riddles in the admin panel. Create riddles of two types:
- Text: players type a free-text answer (case-insensitive matching)
- Multiple choice: players pick from options A/B/C/D
Riddles can optionally include an image.
3. Create a game
Go to New Game, give it a name, set a join code (e.g. TREASURE42), and select which riddles to include (click to toggle, order follows selection order).
4. Share the game link
On the game's live page you'll see the player link, e.g. http://localhost:5173/play/abc12345. Share this link and the join code with players.
5. Players join
Players open the link, enter the join code, pick a team name, and add team member names. They'll land in a waiting room.
6. Start the game
Click Start Game on the admin live page. All waiting players are notified via WebSocket and taken to the play screen.
7. Play
Each team sees the same riddles but in a different shuffled order. They solve riddles one at a time -- get it right to unlock the next one. Wrong answers trigger an exponential cooldown (5s, 10s, 20s, 40s, 80s, max 120s).
The live leaderboard updates in real time for both players and admins.
8. Results
The first team to solve all riddles wins. The admin can also stop the game manually. The results page shows the final leaderboard.
9. Analytics
On the admin game page, click the Analytics tab to see:
- Per-team: riddles solved, total attempts, total time, average time per riddle
- Per-riddle: average time, average attempts, completion rate
Configuration
Environment variables (or .env file in backend/):
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite+aiosqlite:///./caccia_tesoro.db |
Database connection string |
JWT_SECRET |
change-me-in-production |
Secret key for JWT tokens |
JWT_ALGORITHM |
HS256 |
JWT signing algorithm |
JWT_EXPIRY_HOURS |
24 |
JWT token lifetime in hours |
UPLOAD_DIR |
uploads |
Directory for riddle images |
Important: change JWT_SECRET to a random value in production.
API overview
| Endpoint | Auth | Description |
|---|---|---|
POST /api/auth/register |
-- | Create admin account |
POST /api/auth/login |
-- | Get admin JWT |
GET/POST /api/riddles |
Admin | List / create riddles |
GET/PUT/DELETE /api/riddles/{id} |
Admin | Get / update / delete riddle |
GET/POST /api/games |
Admin | List / create games |
POST /api/games/{id}/start |
Admin | Start a game |
POST /api/games/{id}/stop |
Admin | Stop a game |
GET /api/games/{id}/leaderboard |
Admin | Get leaderboard |
GET /api/games/{id}/analytics |
Admin | Get analytics data |
GET /api/play/{slug} |
-- | Game public info |
POST /api/play/{slug}/join |
-- | Join game (returns session token) |
GET /api/play/team/current-riddle |
Team | Get current riddle |
POST /api/play/team/submit |
Team | Submit answer |
GET /api/play/team/status |
Team | Team progress |
GET /api/play/team/leaderboard |
Team | Leaderboard from player side |
WS /api/ws/{slug}?token=... |
Admin/Team | Real-time events |
Full API docs available at http://localhost:8000/docs (Swagger UI) when the backend is running.
Tech stack
- Backend: Python, FastAPI, SQLAlchemy (async), SQLite, python-jose (JWT), bcrypt, Poetry
- Frontend: React 19, TypeScript, Vite, React Router v7
- Real-time: FastAPI WebSockets