How I built a 10,000-seat event ticketing platform in Next.js — the 2D canvas seat-map engine at 39fps

Ustym Kushnir
Ustym Kushnir
··12 min read

Case study of ArTicket: replacing a licensed MuchTicket operation with a custom Next.js + PostgreSQL ticketing platform, own canvas seat-map engine, MercadoPago + Payway signed HMAC webhooks, QR access control, Chatwoot omnichannel support desk on a second VPS.

ArTicket Platform — screenshot

A Mar del Plata operator was renting its ticketing platform. Every stadium event ate 8-12% of gross revenue in per-ticket fees, and any custom flow — a two-tier season pass, a promotor split, a bilingual buyer email — was a "ticket to their support team, come back next week" story. The math stops working once you cross a few hundred events per year.

The problem was not "build a ticketing system"#

The problem was: the operator sells 300-400 events per month across venues that seat 10,000+, and their platform provider decides which features get built, when, and at what fee level. The blocker was ownership, not tickets.

Why building this from scratch made sense#

A ticketing platform sounds like a solved problem until you list the moving parts.

  • Seat map rendering at scale: a 12,000-seat stadium is 12,000 DOM nodes if you use HTML, which is a 3-second first paint and a laggy hover. Canvas is the only viable answer.
  • Concurrent seat holds: two buyers clicking the same seat 200ms apart cannot both succeed. This is a distributed locking problem, and the SaaS providers punt on it (they oversell and refund).
  • Payment provider webhooks: MercadoPago and Payway both send signed HMAC webhooks. If the webhook handler drops a request, you either sell a seat twice or lose the sale entirely.
  • QR access control at the gate: the scanner apps must sync down the current ticket list every 30 seconds, but they also must work offline for 20 minutes when the venue Wi-Fi drops (it always drops).

Every one of those has a "cheap SaaS answer" that fails at 10,000-seat scale.

The stack#

  • Next.js 16 Pages Router as the buyer-facing app, deployed on a DigitalOcean droplet.
  • PostgreSQL 16 + Prisma as the source of truth. All seat inventory is Postgres advisory locks — the same lock the payment webhook takes.
  • Own 2D canvas engine for the seat map, rendering at 39fps on 12,000+ seats even on a mid-range Android.
  • MercadoPago + Payway with signed HMAC webhooks. Every incoming webhook is verified, deduplicated by transaction ID, and idempotent.
  • QR access control with a native scanner app (React Native) that syncs down the current ticket list via a delta API — first sync is full, later syncs only pull what changed.
  • Playwright E2E suite that runs against a Docker Compose replica of production before every deploy.

The canvas engine#

The interesting part is the seat-map renderer. HTML-based renderers hit a wall at ~4,000 seats — the browser cannot lay out that many nodes and still respond to hover / click quickly. Canvas has no such limit, but it also does not give you free click detection.

The engine keeps a spatial hash grid keyed on canvas coordinates. Every seat is a rectangle indexed into the grid at render time. A click at (x, y) hashes to a grid cell, then does a linear check inside that cell (usually 1-8 seats). Zoom and pan re-hash lazily, so you never pay for what the user does not see.

Render is a single requestAnimationFrame loop that redraws only the dirty region (a hovered seat = 40x40 pixels, not the whole 12,000-seat map). On a Pixel 5 I measured 39fps steady during a fast pan across a 12,000-seat stadium.

Concurrent seat holds#

The hard part is not adding a seat to a cart. The hard part is preventing two buyers 200ms apart from both winning.

The design: every "add to cart" attempt takes a Postgres advisory lock keyed on the seat ID. If the lock is free, the seat moves to a per-buyer temporary hold with a 10-minute TTL. If the lock is held, the buyer sees "just taken, pick another" instantly.

The payment webhook takes the same advisory lock. When MercadoPago confirms the payment, the webhook releases the temporary hold and writes the final ticket row atomically. If the payment fails or times out, the hold expires and the seat is available again — no operator intervention.

Signed HMAC webhooks — the boring part that matters#

Both MercadoPago and Payway sign their webhooks. If you skip verification, anyone who guesses your endpoint URL can post fake payment confirmations. The verification is 6 lines of code, and every ticketing SaaS I have seen skips it. We do not.

Chatwoot on a second VPS#

Support was the missing piece. The operator was juggling 7 email mailboxes, a WhatsApp number, a Facebook page and Instagram DMs. I self-hosted Chatwoot on a second DigitalOcean droplet, wired all 10 channels to it, gave 8 agents seats, and set up rclone nightly backups to Google Drive.

Now every support conversation lives in one inbox, agents see the buyer's ticket history at open, and there is no monthly SaaS fee per seat.

What shipped#

  • 300-400 events/month running through the platform.
  • 10,000+ seat stadiums loading in under 2 seconds on 4G.
  • 39fps steady during seat-map interaction.
  • 0 double-booked seats since launch.
  • 0 dropped payment webhooks since launch.
  • 8-12% fee → 0 per-ticket platform cost.

What the client owns now#

Everything. The Next.js repo, the PostgreSQL data, the Chatwoot instance, the QR scanner app source, the deploy scripts. If I disappear tomorrow, any senior full-stack developer can pick up the codebase in an afternoon.

That is the whole point.

Share this article

The project this writeup is about

ArTicket Platform

A ticketing operator was renting its platform. The own 2D canvas seat-map engine runs at 39fps on 10k+ seats, and the operator owns it now.

Related case studies

Want your project written up like this?

See servicesContact