Codeacious intern assignment — concurrency you can see. A small event ticketing system with a deliberate race condition, per-operator isolation, and a nixie-tube instrument. FastAPI + SQLite on the back, React (Vite) on the front — three events, real accounts, and a booking endpoint that breaks the moment two requests arrive together.
┌─────────────────────────────┐ ┌──────────────────────────────┐
│ frontend (React + Vite) │ /api │ backend (FastAPI + SQLite) │
│ http://localhost:5173 │ ─────► │ http://localhost:8000 │
│ nixie-tube booking panel │ proxy │ seeds AI Meetup · capacity 2│
│ operator sign-in /login │ │ + Rust Wkshp · Data Night │
└─────────────────────────────┘ └──────────────────────────────┘
Built by Codeacious as a teaching artifact. The bug is the curriculum.
| Tool | Version | Check | Install |
|---|---|---|---|
| Python | 3.12+ | python3 --version |
https://python.org · brew install python@3.12 |
| Node.js | 22+ | node --version |
https://nodejs.org · brew install node@22 |
| npm | 10+ | npm --version |
ships with Node |
| Docker + Compose | 27+ | docker --version && docker compose version |
https://docs.docker.com/get-docker |
| Git | any | git --version |
https://git-scm.com |
No other dependencies. SQLite ships with Python. All Python/Node deps are pinned in
backend/requirements.txtandfrontend/package.json.
git clone <your-repo-url> tinytickets
cd tinyticketsNo .env file is required. The database is created automatically on first backend start.
Works identically on macOS, Linux, Windows (WSL2).
docker compose up --buildWhat this does:
- Builds
backendimage (python:3.12-slim+requirements.txt) and starts it onhttp://localhost:8000. - Builds
frontendimage (node:22-alpine→nginx:1.27-alpine) and starts it onhttp://localhost:5173. - Creates a named volume
sqlite-datamounted at/app/data/tinytickets.dbso the DB survives restarts. - Backend runs
Base.metadata.create_all+ seeds events and demo operators if the tables are empty.
Open http://localhost:5173. API docs at http://localhost:8000/docs.
Stop: Ctrl+C or docker compose down. Wipe the DB: docker compose down -v (deletes the volume).
cd backend
# create an isolated environment (do this once)
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# install deps (once, or after pulling new changes)
pip install -r requirements.txt
# run the API with auto-reload
uvicorn app.main:app --reload --port 8000Expected log: Uvicorn running on http://127.0.0.1:8000. The file backend/tinytickets.db is created next to backend/app/ on first start.
cd frontend
npm install # once, or after pulling new changes
npm run devExpected log: VITE v6.x ready in ... Local: http://localhost:5173/. The Vite dev server proxies /api → http://localhost:8000 (see frontend/vite.config.ts).
Open http://localhost:5173.
In a third terminal, with the stack running:
# 1. Health
curl -s http://localhost:8000/api/health
# → {"status":"ok"}
# 2. Seeded events exist (AI Meetup has 2 seats free)
curl -s http://localhost:8000/api/events | python3 -m json.tool
# → [{"id":1,"name":"AI Meetup","capacity":2,"seats_left":2}, ...]
# 3. Booking happy path — bookings are per-operator, so sign in first
TOKEN=$(curl -s -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" -d '{"username":"ada","password":"lovelace"}' \
| python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
curl -s -X POST http://localhost:8000/api/events/1/book \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"attendee_name":"Ada"}' | python3 -m json.tool
# third serial booking should 409
# 4. Reset for the assignment
curl -s -X POST http://localhost:8000/api/events/1/reset | python3 -m json.tool
# → {"seats_left":2,"confirmed_bookings":0, ...}Frontend check: sign in as ada / lovelace, open AI Meetup from the event rack, book one seat → BOOKED tube goes 01, third booking shows 409 Sold out.
The database seeds two operators on first start. They also appear as one-click chips on the sign-in page.
| Username | Password | Display name |
|---|---|---|
ada |
lovelace |
Ada Lovelace |
grace |
hopper |
Grace Hopper |
New operators can register via the UI or POST /api/auth/register.
| Symptom | Fix |
|---|---|
Address already in use :8000 / :5173 |
Another app is on that port. Stop it or run uvicorn ... --port 8001 / npm run dev -- --port 5174. |
ModuleNotFoundError in backend |
You forgot source .venv/bin/activate or pip install -r requirements.txt. |
Frontend shows INSTRUMENT OFFLINE |
Backend is not running or is on a different port. Check http://localhost:8000/api/health. |
401 Not authenticated on book/bookings |
Bookings are per-operator — sign in first (see smoke test, or the UI's demo chips). |
tinytickets.db looks stale |
Delete it (rm backend/tinytickets.db natively, docker compose down -v in Docker) and restart — it reseeds automatically. |
npm install fails on Node < 22 |
Upgrade Node. The project pins Vite 6 which requires Node 18+ (22 recommended). |
- Engine: SQLite file database (
backend/tinytickets.dbnatively,/app/data/tinytickets.dbin Docker viaTINYTICKETS_DBenv var — seebackend/app/database.py:14anddocker-compose.yml:9). - Tables:
events(id,name,description,starts_at,capacity,tickets_booked,created_at),users(id,username,display_name,password_hash,created_at),auth_tokens(id,token,user_id,created_at), andbookings(id,event_id,user_id,attendee_name,created_at) — defined inbackend/app/models.py. - Creation:
Base.metadata.create_all(bind=engine)runs on startup insidebackend/app/main.py(seed_database()), then seeds events and demo operators if the tables are empty. - No Alembic migration history is required to run the assignment. For a teaching SQLite DB,
create_allis intentional — it lets an intern clone and run without any migration step.
# native
sqlite3 backend/tinytickets.db "SELECT id, name, capacity, tickets_booked FROM events; SELECT id, username FROM users; SELECT id, attendee_name FROM bookings LIMIT 5;"
# docker
docker compose exec backend sqlite3 /app/data/tinytickets.db "SELECT id, name FROM events;"
# or via the API
curl -s http://localhost:8000/api/events | python3 -m json.tool
TOKEN=$(curl -s -X POST http://localhost:8000/api/auth/login -H "Content-Type: application/json" -d '{"username":"ada","password":"lovelace"}' | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
curl -s http://localhost:8000/api/events/1/bookings -H "Authorization: Bearer $TOKEN" | python3 -m json.tool
curl -s http://localhost:8000/api/my/bookings -H "Authorization: Bearer $TOKEN" | python3 -m json.tool# soft reset (keeps the file, clears bookings for one event, resets counter)
curl -X POST http://localhost:8000/api/events/1/reset
# hard wipe (deletes the file, reseeded on next start)
rm backend/tinytickets.db # native
docker compose down -v # docker (deletes volume)The assignment does not require migrations, but if you add columns/tables, the idiomatic path is Alembic. A minimal setup:
cd backend
source .venv/bin/activate
pip install alembic
alembic init alembic
# edit alembic.ini: sqlalchemy.url = sqlite:///tinytickets.db
# edit alembic/env.py: import app.models; target_metadata = app.database.Base.metadata
alembic revision --autogenerate -m "describe change"
alembic upgrade headAt that point, replace Base.metadata.create_all in backend/app/main.py with alembic upgrade head on startup. The current create_all approach is kept for the assignment because it is one fewer step that can go wrong on a fresh laptop.
Env override for custom DB locations (CI, tests, Docker):
TINYTICKETS_DB=/tmp/test.db uvicorn app.main:app --port 8000
TINYTICKETS_DB=/app/data/tinytickets.db docker compose up| Doc | Audience |
|---|---|
docs/architecture.md |
How the system works, end to end |
docs/api.md |
Current API reference with examples |
TASK.md |
Intern assignment sheet (3 timed tasks, ~1 hour) |
AGENTS.md / CLAUDE.md |
Conventions for AI coding agents working in this repo |
tinytickets/ ← Codeacious · TinyTickets
├── README.md ← you are here (setup + DB + overview)
├── TASK.md ← intern task sheet (hand this out)
├── docker-compose.yml ← one-command stack
├── backend/
│ ├── Dockerfile ← python:3.12-slim → uvicorn
│ ├── requirements.txt ← pinned FastAPI / SQLAlchemy / Pydantic
│ ├── README.md ← API reference (endpoint table)
│ └── app/
│ ├── main.py ← FastAPI app + lifespan seeding
│ ├── database.py ← engine/session, TINYTICKETS_DB override
│ ├── models.py ← Event, User, AuthToken, Booking ORM models
│ ├── security.py ← password hashing (pbkdf2) + token minting
│ ├── deps.py ← get_current_user dependency
│ ├── schemas.py ← Pydantic request/response models
│ └── routers/
│ ├── events.py ← GET list · POST book · reset helper (the race lives here)
│ ├── auth.py ← POST register · POST login · GET me
│ └── me.py ← GET my bookings (cross-event, per-operator)
└── frontend/
├── Dockerfile ← node:22 build → nginx:1.27 serve
├── nginx.conf ← serves SPA, proxies /api → backend:8000
├── vite.config.ts ← dev proxy /api → :8000
└── src/
├── App.tsx ← shell + nav + route table + auth context
├── main.tsx ← BrowserRouter boot
├── api.ts ← typed fetch wrappers (token attached inside)
├── types.ts ← shared TypeScript types
├── pages/
│ ├── LoginPage.tsx ← operator sign-in / register + demo chips
│ ├── EventsPage.tsx ← event rack
│ ├── EventPage.tsx ← nixie instrument + booking + stress + ledger
│ └── BookingsPage.tsx ← my bookings across events
├── components/
│ ├── NixieTube.tsx ← tube + tube bank (live capacity digits)
│ └── Ledger.tsx ← request ledger (the evidence)
└── styles/global.css ← the whole visual world (nixie lab counter)
The intern sheet in TASK.md runs three timed tasks (~60 minutes total):
orient with a coding agent, reproduce + fix the booking race, then ship a
Cancel + Waitlist feature (backend + UI).
The booking endpoint POST /api/events/{id}/book in backend/app/routers/events.py contains a deliberate concurrency flaw: it fetches the current count, checks capacity in Python, sleeps 0.15s, then writes the new count back — with no atomic SQL, no transaction, no lock. Under concurrent load, ten parallel requests can all read 0 before any of them writes 1, so the counter and the booking rows disagree.
Mentors: see docs/mentor-notes.md (spoilers — not for interns).
Base URL: http://localhost:8000 (interactive docs at /docs).
| Method | Path | Body | Success | Auth | Failure |
|---|---|---|---|---|---|
| GET | /api/events |
— | 200 |
— | — |
| POST | /api/events/{id}/book |
{"attendee_name": "…"} |
201 |
Yes | 401, 404, 409 |
| GET | /api/events/{id}/bookings |
— | 200 |
Yes | 401, 404 |
| GET | /api/my/bookings |
— | 200 |
Yes | 401 |
| POST | /api/events/{id}/reset |
— | 200 |
— | 404 |
| POST | /api/auth/register |
{"username": "…", "password": "…", "display_name": "…"} |
201 |
— | 409 |
| POST | /api/auth/login |
{"username": "…", "password": "…"} |
200 |
— | 401 |
| GET | /api/auth/me |
— | 200 |
Yes | 401 |
| GET | /api/health |
— | 200 |
— | — |
Auth is Authorization: Bearer <token> (session token returned by register/login,
stored in localStorage on the frontend).
GET /api/events response fields:
| Field | Meaning |
|---|---|
capacity |
Seat limit |
tickets_booked |
Running counter maintained by the booking endpoint |
confirmed_bookings |
COUNT(*) of booking rows — the ground truth |
seats_left |
capacity − confirmed_bookings; negative means overbooked |
Under concurrent bookings, tickets_booked and confirmed_bookings diverge. That divergence is the assignment.
- The flaw is confined to
book_ticketinbackend/app/routers/events.py: fetch → check in Python → write back. No atomicUPDATE ... WHERE, noSELECT ... FOR UPDATE, no serialization. - The
0.15ssleep (BOOKING_LATENCY_SECONDS) models real booking work (payments, seat holds) and widens the race window soFIRE 10 REQUESTSreliably interleaves. Do not accept "remove the sleep" as a fix — the race exists without it. - The frontend is a demonstration instrument (nixie laboratory counter), not the grading surface — but with per-operator isolation, the book and bookings endpoints now require auth, so any manual API grading must include a bearer token (see smoke test).
- Grading signal for a correct fix: repeated
reset → fire 10(signed in as any operator) always ends2 confirmed · 8 rejected · seats_left 0, bothtickets_bookedandconfirmed_bookingsagree, and the happy-path third booking still409s.GET /api/my/bookingsand event bookings are per-operator.
© Codeacious. This repository is an internal teaching artifact. Do not distribute outside Codeacious without permission.