Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TinyTickets · Codeacious

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.


Prerequisites (fresh laptop)

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.txt and frontend/package.json.


Run on a new laptop — step by step

0. Clone

git clone <your-repo-url> tinytickets
cd tinytickets

No .env file is required. The database is created automatically on first backend start.

Option A — Docker (recommended, one command)

Works identically on macOS, Linux, Windows (WSL2).

docker compose up --build

What this does:

  1. Builds backend image (python:3.12-slim + requirements.txt) and starts it on http://localhost:8000.
  2. Builds frontend image (node:22-alpinenginx:1.27-alpine) and starts it on http://localhost:5173.
  3. Creates a named volume sqlite-data mounted at /app/data/tinytickets.db so the DB survives restarts.
  4. 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).

Option B — Native (two terminals, no Docker)

Terminal 1 — backend

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 8000

Expected log: Uvicorn running on http://127.0.0.1:8000. The file backend/tinytickets.db is created next to backend/app/ on first start.

Terminal 2 — frontend

cd frontend
npm install          # once, or after pulling new changes
npm run dev

Expected log: VITE v6.x ready in ... Local: http://localhost:5173/. The Vite dev server proxies /apihttp://localhost:8000 (see frontend/vite.config.ts).

Open http://localhost:5173.

Verify it worked (30-second smoke test)

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.

Demo operators

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.

Troubleshooting

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).

Database & migrations

How it works today (zero-config for interns)

  • Engine: SQLite file database (backend/tinytickets.db natively, /app/data/tinytickets.db in Docker via TINYTICKETS_DB env var — see backend/app/database.py:14 and docker-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), and bookings (id, event_id, user_id, attendee_name, created_at) — defined in backend/app/models.py.
  • Creation: Base.metadata.create_all(bind=engine) runs on startup inside backend/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_all is intentional — it lets an intern clone and run without any migration step.

Inspecting the DB

# 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

Resetting / wiping

# 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)

If you evolve the schema (optional Alembic path)

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 head

At 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

Documentation

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

Project structure

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 assignment (summary)

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).


API reference

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.


Notes for reviewers / mentors (Codeacious)

  • The flaw is confined to book_ticket in backend/app/routers/events.py: fetch → check in Python → write back. No atomic UPDATE ... WHERE, no SELECT ... FOR UPDATE, no serialization.
  • The 0.15s sleep (BOOKING_LATENCY_SECONDS) models real booking work (payments, seat holds) and widens the race window so FIRE 10 REQUESTS reliably 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 ends 2 confirmed · 8 rejected · seats_left 0, both tickets_booked and confirmed_bookings agree, and the happy-path third booking still 409s. GET /api/my/bookings and event bookings are per-operator.

License & ownership

© Codeacious. This repository is an internal teaching artifact. Do not distribute outside Codeacious without permission.

About

TinyTickets - a concurrency teaching instrument: FastAPI + SQLite backend, React nixie-tube frontend, and a booking endpoint with a deliberate race condition (Codeacious intern assignment)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages