Case study · Engineering

Wooziee: the engineering problems we hit, and how we solved them

Building a ticketing platform is easy until real money, real gates and real crowds arrive. These are the hardest problems we faced building Wooziee for India and the UK, and the decisions that fixed them.

Team
5 engineers, founder-led
Platforms
Web, admin, Android, iOS
Markets
India and UK
Scale of build
~260 endpoints, 49 models

Challenge 01

Customers paid, but orders still said "unpaid"

The problem

Early on we confirmed orders from the browser's return after checkout. If the customer closed the tab, lost signal or the redirect failed, the money was captured but the order never moved to paid, so no ticket was issued and support had to fix it by hand. Retries could also create the opposite risk: one payment issuing two tickets.

How we solved it

  • Made the provider's signed webhook the only source of truth for "paid".
  • Made order updates idempotent, so a repeated webhook cannot issue tickets twice.
  • Added a scheduled reconciliation against the provider to catch missed events.
  • Kept the browser return for user experience only, never for state.

We wrote up the full pattern in webhook-verified payments.

Challenge 02

Multi-day tickets at a busy gate

The problem

Festivals sell passes valid for several days. A simple "scanned = used" flag either blocked a valid pass on day two or let the same pass in twice on the same day. Gate staff also needed an answer in under a second, on mid-range phones.

How we solved it

  • Recorded each scan with its event day instead of a single used flag.
  • Validated against day-level ticket types and inventory, so a pass is valid for exactly the remaining days.
  • Gave organisers a per-day entry history for disputes and headcounts.
  • Pushed scans live to the organiser dashboard over Socket.IO.

Challenge 03

Permissions that drifted between API and admin

The problem

Four roles (platform admin, organiser admin, organiser staff and attendee) plus custom staff roles meant permission checks spread across dozens of route modules and screens. When the API and admin UI disagreed, staff saw buttons they could not use, or worse, the API allowed something the UI hid.

How we solved it

  • Defined one permission catalogue shared by the API and the admin UI.
  • Checked every route against it centrally, including the organisation's status.
  • Revoked sessions instantly with JWTs plus a Redis blacklist when roles changed.

Challenge 04

Two countries, two payment providers, one platform

The problem

India runs on Razorpay with Razorpay X payouts; the UK edition needed Stripe. Different currencies, fees, payout flows and webhook formats threatened to fork the codebase.

How we solved it

  • Put payments behind a provider-neutral layer with one order and payout model.
  • Configured currency, fees and payouts per market, not per code path.
  • Normalised each provider's webhooks into the same internal events.

Challenge 05

Event pages search engines could not read

The problem

The React app rendered event details in the browser. Search engines, social previews and AI assistants saw an almost empty page, so shared links looked broken and events did not appear in search.

How we solved it

  • Served server-rendered event pages with full details, Open Graph images and structured data.
  • Kept the interactive React checkout on top.
  • Added automated tests for SEO and AI-crawler access to every release.

Challenge 06

On-sale spikes and slow background work

The problem

When tickets go on sale, traffic arrives at once. Sending emails, push notifications and PDF tickets inside the request made checkout slow exactly when it mattered most.

How we solved it

  • Moved email, push, reminders and backups to Bull queues on Redis.
  • Cached hot reads in Redis and served media through CloudFront.
  • Added rate limiting, Sentry error tracking and New Relic APM to see problems before customers did.

Challenge 07

Handling identity documents safely

The problem

Concession tickets required ID verification, which meant storing sensitive documents that should not stay public or forever.

How we solved it

  • Stored documents privately and served them with short-lived signed URLs.
  • Logged every view.
  • Deleted documents automatically after the event.

Lessons

What we would do again on any platform

  • Trust verified server events, never the browser, for money.
  • Model real-world events (each scan, each payment) instead of overwriting flags.
  • Keep one source of truth for permissions.
  • Abstract providers early when more than one market is likely.
  • Test SEO, accessibility and crawler access in CI, like any other feature.

Wooziee is live at wooziee.com and wooziee.co.uk. See the project overview for screens and architecture.

FAQ

Quick answers

How do you stop paid orders showing as unpaid?

Treat the payment provider's verified webhook as the source of truth, make order updates idempotent so retries cannot double-issue tickets, and run a scheduled reconciliation against the provider's records to catch anything a webhook missed.

How do multi-day tickets work at the gate?

Each ticket carries a QR code tied to the order. Every scan is recorded with the event day, so a multi-day ticket stays valid for the remaining days while the same day cannot be entered twice.

How did Wooziee support India and the UK?

Payments sit behind a provider-neutral layer: Razorpay and Razorpay X in India, Stripe in the UK. Currency, fees and payouts are configured per market, so the rest of the platform does not care which provider handled the money.

Next step

Tell Us Your Problem. We'll Help You Build the Solution.

Tell us what you are working on. You will get a considered reply from an engineer — with questions, a suggested approach or an honest “we are not the right fit”.

Book a call