Skip to content

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.

Gişe’s live seat map for the play “Martı”: seats coloured by price tier, two seats selected and a ₺1,530 total.
The live seat map — held and sold seats update as others shop.

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

  1. 1Discover

    “What’s on this week?” — a week strip, categories and cities, Turkish-first with an English toggle.

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

  3. 3Hold & pay

    The seats are held for seven minutes with a countdown; card payment goes through a 3-D Secure step.

  4. 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”.

  5. 5At the door

    Staff scan with the PWA: green, red or amber. Scans queue offline and sync later — the first scan wins.

Key decisions

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

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

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

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

CLIENTSAudience appReact · ViteVenue paneldashboard · map editorDoor scannerPWA · offline queueHTTPS /api · WebSocketsEDGECloudflarePages · Workers · rate limitsAPPLICATIONHono APIcheckout · webhooks ·scan syncSeatInventory DO1 per show session ·holds · alarmsWaitingRoom DOfair queue · signedadmissionSQL · RPCDATAD1orders · tickets ·payments · check-insDO SQLiteseat state — the onlywriterOUTSIDEPaymentProviderfake PSP ·iyzico-shapedCron 04:00nightly demo reset↑ ships everything aboveDELIVERYWorkers testsVitest · real DOs + D1Domain tests26 unit testsk6 race2,000 buyers · verifywrangler deploydry-run in every build

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

Want the longer version — the code, the pipeline, the trade-offs?

Email me
Next case studyTahsilReceivables and collections for Turkish SMBs: customer ledgers, invoices, polite reminder sequences, pay-by-link and reconciliation — on a double-entry ledger that always balances.