Case study · Personal project · live · open source
Daur, race registration that stays correct under load
A full-stack platform for closed-road city races. The checkout holds three invariants however many people press Confirm in the same second.
- Scope
- 19 pages15 route handlers, 16 Prisma models
- Tests
- 100 unit tests(Vitest) plus a Playwright registration flow
- Design system
- enforced by a build checkTokens are the only source of colour
- Status
- Live on Vercel with Neon Postgres
Why I built it
I run: 10+ races in Kolkata, mostly 10Ks, and 2 half marathons. City races on closed roads have hard capacity limits, and registration is where things can break: oversold categories, duplicate entries, half-registered groups. Two reasons made it worth building:
- My day job is the frontend of someone else's API. I wanted the whole stack: schema design, auth, transactions and deployment.
- Registration is a genuinely concurrent problem: small enough to finish, hard enough to be interesting.
What it does
A runner finds a race, registers themselves and the people they run with, and gets a reference number and a calendar invite. An organizer creates the event, sets distances and capacity, opens and closes registration, and sees who has entered.

Race listing 
Event detail 
Registration
The hard part: a checkout that can't oversell
Three invariants have to hold however many people press Confirm in the same second:
- A category never sells more slots than it has.
- A person is never registered twice for the same event.
- A group checkout is all-or-nothing.
None of them rests on application logic. Checking capacity with a SELECT and then inserting is a race: two requests both read "1 slot left" and both insert.
- Capacity is a conditional update, not read-then-write.
When the category is full the statement matches zero rows, and that zero is the rejection. There's no window between checking and taking.UPDATE "RaceCategory" SET "slotsTaken" = "slotsTaken" + $n WHERE "id" = $id AND "slotsTaken" + $n <= "slotLimit" - Duplicates are rejected by a partial unique index on confirmed registrations. A plain unique constraint would also block someone re-registering after cancelling. Prisma can't express a partial index, so it lives in raw SQL in the migration, with the reason written above it.
- Atomicity is one transaction.
- References (like
DBE26-0042) are minted in the same transaction by reserving a block of the event's sequence, so two checkouts can never mint the same number. - Replays are idempotent. The client's idempotency key becomes the group id, so a double-submitted checkout finds the existing group instead of creating a second one.
Typed service errors (event not open, category not found, participant not yours, underage) map to precise messages in the UI.
Interactive · coming next
Press Confirm with 50 people
"Press Confirm with 50 people": a small simulation with a slider for concurrent buyers against a category with N slots. It runs side by side:
- Read-then-write oversells.
- The conditional update never does.
It's pure client-side and illustrative, and matches the SQL above.
Decisions I'd defend
Event status is derived, not trusted
The organizer sets a status, but the open and close timestamps can only narrow it. An event left "open" past its closing time stops taking entries even if no job has run.
References never change
The event prefix is stored, not derived, so renaming an event never alters references already printed on confirmations.
The rate limiter is honest about its limits
It's a fixed-window, in-memory speed bump on auth routes. Its weaknesses (per-instance state, lost on cold start) are written in the code rather than overstated.
Design tokens are enforced by a script, not by discipline
The build fails if a raw hex colour appears outside
tokens.css, and a second check verifies contrast ratios. It's the same instinct as the boundary rule on the UI platform: if a rule matters, a machine should check it.
What I’d do differently
- Move the rate limiter to Redis before real traffic.
- End-to-end coverage is one registration flow. The organizer path is only covered by unit tests.
- Measure and budget images properly before a real launch.