Case studyLive
Gişe
Seat-map ticketing for small theatres, stand-up clubs and concert halls in Türkiye — where no seat is ever sold twice.
- Role
- Solo — product, design implementation, backend, infra
- When
- 2026
- Backend
- Full-stack
Buy a ticket in about a minute, no sign-up: test card 4242 4242 4242 4242, any future expiry, CVC 123, 3-D Secure code 123456. The demo resets every night; the source is private.

Who it’s for
- Venues of 120–420 seats in places like Kadıköy or Alsancak that sell through a generic marketplace or a spreadsheet today.
- They need three things big platforms don’t give them: their own seat map (boxes, wheelchair spaces, restricted-view seats, a licence capacity), fair on-sales when a show is hot, and a door app that works in a basement with no signal.
- Audiences who want to pick a seat on their phone, pay with 3-D Secure and get a QR ticket that just works.
What I built
- Built it alone as a complete, operated product: a Hono API on Cloudflare Workers, D1, and two SQLite-backed Durable Object classes — one that owns each show session’s seats, one that runs the waiting room for hot sessions.
- The audience app in React + Vite, ported 1:1 from an approved design prototype: discover, a live seat map (pan, pinch, keyboard), seven-minute holds, 3-D Secure checkout, an HMAC-signed QR e-ticket that opens offline, PDF and calendar file, and the waiting room.
- The venue side: a versioned seat-map editor (sections, curves, aisles, price brush, accessibility, licence checks) and a sales dashboard — plus a door-scanner PWA with an offline queue where the first scan wins.
- Payments behind an iyzico-shaped PaymentProvider with a built-in fake PSP that sends 2–3 duplicate webhooks on purpose; a nightly cron that resets the demo, rate limits, structured logs, health and metrics — and the tests and load test that prove the invariant.
How it works
1Discover
“What’s on this week?” — a week strip, categories and cities, Turkish-first with an English toggle.
2Pick seats
A live seat map over WebSockets: held and sold seats update as others shop. High-demand shows go through a waiting room first.
3Hold & pay
The seats are held for seven minutes with a countdown; card payment goes through a 3-D Secure step.
4Get the ticket
An HMAC-signed QR e-ticket on a wallet-style page that opens offline, a printable PDF, a calendar file and “My tickets”.
5At the door
Staff scan with the PWA: green, red or amber. Scans queue offline and sync later — the first scan wins.
Key decisions
01
One Durable Object per show session owns its seats
Instead of: Row locks or optimistic checks in a shared database.
Why: Every hold, expiry and sale for a session runs inside one single-threaded object with synchronous SQLite transactions, so a seat can’t be sold twice by construction — there is no read-then-write window for two requests to share. Holds expire through alarms, live updates fan out over hibernating WebSockets, and a unique index in D1 is the second line of defence.
02
Idempotent payments; a late payment is refunded, never double-booked
Instead of: Trust the browser callback and the first webhook that arrives.
Why: Checkout takes an Idempotency-Key, confirming an order is idempotent, webhooks are deduplicated by event id and every status change is guarded. If a hold expired during 3-D Secure and someone else bought the seat, the payment is refunded automatically. The fake PSP sends duplicate webhooks on every purchase, so this path is exercised all the time.
03
A fair waiting room in front of hot on-sales
Instead of: First come, first served at the seat map.
Why: Everyone who arrives before the on-sale moment is shuffled — arriving at 11:58 doesn’t beat 11:59 — and later arrivals are FIFO. A token bucket caps admissions per minute and concurrent shoppers; admission is a signed, expiring token bound to the buyer, and the seat endpoint refuses a hot session without it.
04
Tickets that verify offline
Instead of: Check every scan against the server.
Why: QR codes are HMAC-signed with a per-session key; door devices download only the key and ticket list for the session they work. Scans are verified locally, queued and synced, and the server keeps the earliest scan. The trade-off — a compromised scanner could mint tickets for that one session — is documented, with Ed25519 as the upgrade path.
Architecture
Seat state lives in exactly one place — the session’s Durable Object; D1 is the second line of defence. Everything here is mine.
Numbers
- seats sold twice — k6, 2,000 buyers for 120 seats
- 0
- duplicate webhook deliveries dropped (438 for 96 orders)
- 342
- tests: 14 in the Workers runtime, 26 domain unit tests
- 40
- a month — Cloudflare’s free plan
- €0
Tech stack
Backend
- Cloudflare Workers
- Durable Objects
- D1 (SQLite)
- Hono
- TypeScript
- WebSocket hibernation
- Cron Triggers
- Rate Limiting
Frontend
- React
- React Router
- Vite
- PWA · service worker
- jsQR
- qrcode-generator
Payments & security
- 3-D Secure flow
- Idempotency keys
- HMAC-SHA256
- PBKDF2
Quality
- Vitest (Workers pool)
- k6
- wrangler dry-run
Gallery

Checkout with a 3-D Secure step — the fake PSP behaves like a real one, duplicate webhooks included. 
On a phone: the waiting room, the seat map and the e-ticket. 
The seat-map editor: versioned publish, blocked when the licence capacity is exceeded. 
The venue’s sales dashboard. 
The door scanner — works offline, and the first scan wins.
Want the longer version — the code, the pipeline, the trade-offs?
Email me