Skip to content

Repository files navigation

🎟️ TicketFlow

A full-stack event ticketing platform built for speed, safety, and scale.

Browse events β†’ Select seats β†’ Pay β†’ Get a QR ticket. Concurrency-safe, transaction-backed, zero race conditions.

Getting Started Β· Architecture Β· API Reference Β· Load Testing Β· Roadmap

Screenshot 2026-07-10 at 2 34 47β€―AM

✨ Features

Area What it does
Seat Selection Real-time seat map with 8s polling, server-side hold locks, conflict detection
Atomic Booking Single DB transaction β€” Booking + Tickets + Payment + Discount in one shot
QR Tickets Generated on confirmation; scannable at the gate via admin scanner UI
Admin Dashboard Event management, revenue analytics, gate check-in
Concurrency Safety Unique DB constraints + seat lock service; verified with a 25-user load test
Auth JWT in httpOnly cookies; no NextAuth, no OAuth β€” just 150 lines that work

Tech Stack

Frontend    Next.js 16 (App Router) Β· React Server Components Β· Tailwind Β· Framer Motion + GSAP Β· Zustand
Backend     Next.js API Routes Β· MySQL via mysql2/promise Β· JWT Β· bcrypt
Validation  Zod (everywhere)
Database    MySQL 8 β€” raw SQL, no ORM

No NextAuth. No Prisma. Both add dependencies and learning overhead that this scale doesn't justify. The auth layer is ~150 lines; the query layer is plain SQL.


Getting Started

Prerequisites

  • Node.js β‰₯ 18
  • MySQL 8 database named ticket_booking_system

Installation

# 1. Clone and install
git clone https://github.com/your-org/ticketflow.git
cd ticketflow
npm install

# 2. Configure environment
cp .env.example .env.local

Edit .env.local:

DB_HOST=localhost
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=ticket_booking_system
AUTH_SECRET=         # openssl rand -base64 32
# 3. Apply schema (run once)
mysql -u root -p ticket_booking_system < scripts/01_schema_patches.sql

# 4. Start dev server
npm run dev

Open http://localhost:3000.

Seeding an Admin User

Option A β€” SQL direct:

-- 1. Generate a bcrypt hash for your password first (cost factor 10+)
INSERT INTO Users (name, email, phone, password, is_admin)
VALUES ('Your Name', 'you@example.com', '9000000000', '<bcrypt-hash>', 1);

INSERT INTO Admin (name, email)
VALUES ('Your Name', 'you@example.com');

Option B β€” Sign up then promote:

# Register via /auth/register, then run:
mysql -u root -p ticket_booking_system <<'SQL'
  UPDATE Users SET is_admin = 1 WHERE email = 'you@example.com';
  INSERT INTO Admin (name, email)
    SELECT name, email FROM Users WHERE email = 'you@example.com';
SQL

Architecture

The system is divided into three layers β€” Client, Server, and Data β€” with a strict one-way dependency: the client talks only to API routes, API routes talk only to the service/query layer, and only the service layer touches MySQL.

flowchart TB
    subgraph CLIENT["🌐 Client Layer"]
        direction LR
        EP["Events page\n── 8s seat poll"]
        BF["Booking flow\n── seat picker Β· payment"]
        CF["Confirmation\n── QR ticket Β· 3D reveal"]
        PR["Profile\n── history Β· settings"]
        AD["Admin\n── analytics Β· QR scanner"]
    end

    subgraph STATE["🧠 Client State"]
        ZS["Zustand Store\nbookingStore Β· userStore"]
        HK["Custom Hooks\nuseBookingFlow Β· useSeatMap"]
    end

    subgraph API["βš™οΈ Next.js API Routes"]
        R1["GET  /api/seats/[event_id]\nPOST /api/seats/lock"]
        R2["POST /api/book-ticket\n── atomic Β· server-computed price"]
        R3["POST /api/payment\n── server-computed amount"]
        R4["POST /api/discount\n── code validation"]
        R5["POST /api/review"]
        R6["GET  /api/profile/[user_id]"]
        R7["GET  /api/confirmation/[booking_id]"]
        R8["POST /api/auth/signup\nPOST /api/auth/login"]
        R9["Admin /api/admin/*\n── requireAdmin() guard"]
    end

    subgraph AUTH["πŸ” Auth Service  ──  lib/auth/session.ts"]
        J["JWT Β· httpOnly cookie\nHS256 Β· SameSite=Lax Β· Secure"]
        BC["bcrypt Β· password hash\nauto-upgrades plaintext on login"]
        GD["getSession() Β· requireUser()\nrequireAdmin()"]
    end

    subgraph LOCK["πŸ”’ Seat Lock Service  ──  lib/services/seatLock.ts"]
        SL["10-min TTL holds\nINSERT ON DUPLICATE KEY UPDATE\n409 on race conflict"]
    end

    subgraph TXN["⚑ Atomic Booking Transaction  ──  one MySQL transaction, ROLLBACK on failure"]
        direction LR
        T1["1Β· Verify\nlocks owned\nby user"] --> T2["2Β· Price\nserver-side\nno client amt"] --> T3["3Β· Validate\ndiscount\ncode"] --> T4["4Β· INSERT\nBooking Β· Tickets\nPayment Β· Log"] --> T5["5Β· Delete locks\nCOMMIT\nor ROLLBACK"]
    end

    subgraph DB["πŸ—„οΈ MySQL 8  ──  raw SQL, no ORM"]
        direction LR
        U["Users\nis_admin β˜…"]
        ADM["Admin"]
        EV["Event\nidx:event_date"]
        VN["Venue"]
        CT["Category"]
        OR["Organizer"]
        ST["Seat"]
        SLT["Seat_Lock\nUNIQUE β˜…\n(seat+event)"]
        BK["Booking\nidx:user+date"]
        TK["Ticket\nUNIQUE β˜…\n(event+seat)"]
        PM["Payment\nstatus=Completed"]
        DS["Discount /\nBooking_Discount"]
        TL["Txn_Log\nCHECKIN audit"]
        RV["Review"]
    end

    %% Client β†’ State
    BF & EP --> ZS & HK

    %% Client β†’ API
    EP --> R1
    BF --> R2
    CF --> R7
    PR --> R6
    AD --> R9

    %% API β†’ Auth
    R8 --> J & BC
    R9 --> GD
    R6 --> GD

    %% API β†’ Seat Lock
    R1 --> SL

    %% API β†’ Atomic Txn
    R2 --> T1

    %% Atomic Txn β†’ DB
    T4 --> BK & TK & PM & DS & TL
    T5 --> SLT

    %% Auth β†’ DB
    BC --> U
    GD --> U

    %% API β†’ DB direct reads
    R1 --> ST
    R3 --> PM
    R4 --> DS
    R5 --> RV
    R7 --> BK
    R9 --> TL
Loading

β˜… UNIQUE constraint enforced at DB level β€” the real safety net against double-bookings. i INDEX on hot query columns β€” matches every frequent read path.


Auth β€” JWT in an httpOnly Cookie

All auth lives in lib/auth/session.ts (~150 lines).

  • Login signs an HS256 JWT containing { user_id, email, name, is_admin }
  • Delivered via Set-Cookie: session=...; HttpOnly; SameSite=Lax; Secure
  • Server helpers getSession(), requireUser(), requireAdmin() read and verify it
  • The client never sees the token

Plaintext password migration: Legacy seed data used plaintext passwords. The login route detects bcrypt vs. plaintext via regex and auto-upgrades on successful login β€” zero downtime migration.


Booking β€” One Transaction, Server-Computed Price

The original flow made 4 sequential client-side POSTs (/booking β†’ /ticket Γ— N β†’ /payment β†’ /booking-discount). Any failure mid-flight left the DB inconsistent. Worse, the client sent the payment amount β€” trivially exploitable.

The new /api/book-ticket route does all of this atomically:

1. Verify user owns active locks on every requested seat
2. Look up seat numbers + compute price server-side from SEAT_PRICE config
3. Validate discount code against the DB
4. INSERT: Booking β†’ Tickets β†’ Payment β†’ Booking_Discount β†’ Transaction_Log
5. DELETE seat locks
6. COMMIT β€” or ROLLBACK on any failure

Returns 409 Conflict with the conflicting seat list so clients can recover gracefully.

Screenshot 2026-07-10 at 2 34 13β€―AM Screenshot 2026-07-10 at 2 30 27β€―AM Screenshot 2026-07-10 at 2 31 39β€―AM

Seat Locks β€” Atomic Acquire

lib/services/seatLock.ts β€” backed by Seat_Lock table with UNIQUE(seat_id, event_id).

Acquire flow:

BEGIN TRANSACTION
  β†’ Sweep expired locks for requested seats
  β†’ SELECT ... FOR UPDATE β€” inspect remaining locks
  β†’ Reject if any lock belongs to another user          β†’ 409
  β†’ Reject if a Ticket already exists for these seats   β†’ 409
  β†’ INSERT ... ON DUPLICATE KEY UPDATE (claim or refresh)
COMMIT

If two users race for the same seat simultaneously, exactly one wins. The other receives a 409 with the conflicting seat IDs.


Schema Patches β€” Preventing Entire Bug Classes

scripts/01_schema_patches.sql adds:

Constraint / Index Table Why it matters
UNIQUE(event_id, seat_id) Ticket The actual double-booking guard β€” FOR UPDATE on a non-existent row provides no protection
UNIQUE(seat_id, event_id) Seat_Lock Enables atomic upsert; prevents duplicate locks
is_admin flag Users Replaces the spoofable x-admin-email request header
Index on booking_date Booking Matches the hot query path for user booking history
Index on event_date Event Fast upcoming-events filter
Index on (booking_id, seat_id) Ticket Efficient ticket lookup by booking
Index on expiry_time Seat_Lock Fast sweep of expired locks

The unique constraint on Ticket is the real safety net. Application-level locks are defence-in-depth; the DB constraint is what actually prevents double-bookings.


Seat Map β€” Real-Time Without WebSockets

The seat picker polls /api/seats/[event_id] every 8 seconds while a user is selecting.

  • No WebSocket infrastructure
  • Latency indistinguishable to the user at this scale
  • To upgrade: swap setInterval for a Server-Sent Events stream on the same endpoint

Analytics Dashboard

/api/admin/analytics runs 6 aggregation queries in parallel:

  • KPIs (total revenue, bookings, tickets sold)
  • Revenue by event
  • Revenue by category
  • Revenue by payment method
  • 30-day daily trend
  • Top events by occupancy

All revenue is keyed off Payment.status = 'Completed' β€” abandoned bookings never pollute the numbers. The UI renders charts using custom SVG β€” no chart library dependency.


QR Ticket Validation

Admin scanner UI at /admin/scanner:

  1. Inspect β€” paste or scan a QR code to preview ticket details (event, seat, holder, payment status)
  2. Check In β€” mark the ticket used; recorded in Transaction_Log with action_type = 'CHECKIN-{ticket_id}'
  3. Re-scan detection β€” already-checked-in tickets are flagged immediately

For live venues, the paste input already accepts USB barcode readers (they present as keyboards). Camera-based scanning: drop in html5-qrcode.


Project Structure

app/
β”œβ”€β”€ (app)/                        Shared navbar/footer layout group
β”‚   β”œβ”€β”€ admin/
β”‚   β”‚   β”œβ”€β”€ page.tsx              Event management + bookings table
β”‚   β”‚   β”œβ”€β”€ analytics/page.tsx    Revenue charts + KPIs
β”‚   β”‚   β”œβ”€β”€ scanner/page.tsx      QR check-in
β”‚   β”‚   └── layout.tsx            Tab nav + admin guard
β”‚   β”œβ”€β”€ booking/[event_id]/
β”‚   β”‚   └── BookingClient.tsx     Seat picker β†’ review β†’ payment flow
β”‚   β”œβ”€β”€ confirmation/[booking_id]/page.tsx
β”‚   β”œβ”€β”€ events/                   Listing + detail pages
β”‚   └── profile/                  Bookings, reviews, settings
β”‚
api/
β”œβ”€β”€ auth/{login,signup,logout,me}/route.ts
β”œβ”€β”€ admin/{events,bookings,analytics,validate-ticket}/route.ts
β”œβ”€β”€ book-ticket/route.ts          Atomic booking endpoint
β”œβ”€β”€ seats/{[event_id],lock}/route.ts
└── events/[id]/route.ts
β”‚
lib/
β”œβ”€β”€ auth/session.ts               JWT helpers, requireUser, requireAdmin
β”œβ”€β”€ services/seatLock.ts          Acquire / release / validate locks
β”œβ”€β”€ queries/                      Read-only DB queries
β”œβ”€β”€ store/                        Zustand stores (user, booking)
β”œβ”€β”€ validations/                  Zod schemas
β”œβ”€β”€ hooks/                        useBookingFlow, useSeatMap
β”œβ”€β”€ utils/                        formatDate, formatPrice, generateQR
└── db.ts                         mysql2 connection pool
β”‚
components/
β”œβ”€β”€ payment/PaymentStep.tsx       Card form, OTP modal, atomic submit
β”œβ”€β”€ confirmation/Ticket3D.tsx     Animated QR ticket
└── layout/ ui/ shared/
β”‚
scripts/
β”œβ”€β”€ 01_schema_patches.sql         Run once on new DB
└── load-test-booking.mjs         Concurrency proof

Load Testing

Proves concurrency safety under simultaneous competing requests.

# Terminal 1
npm run dev

# Terminal 2
EVENT_ID=1 SEAT_ID=1 CONCURRENT=25 node scripts/load-test-booking.mjs

Spins up 25 concurrent users, each attempting to book the same seat at the same instant.

────────── Results ──────────
Total time:       312ms
Successes (201):  1
Conflicts (409):  24
Lock rejections:  0
Other:            0
──────────────────────────────
βœ… PASS β€” Exactly one booking succeeded. Concurrency is safe.

Set FULL_FLOW=1 to stress the lock service path. Without it, requests go directly to /api/book-ticket and stress the unique constraint.


API Reference

Auth

Method Endpoint Description
POST /api/auth/login Sign in, set session cookie
POST /api/auth/signup Register new user
POST /api/auth/logout Clear session cookie
GET /api/auth/me Get current session user

Events & Seats

Method Endpoint Description
GET /api/events/[id] Event detail
GET /api/seats/[event_id] Seat map with lock status
POST /api/seats/lock Acquire seat hold (10 min TTL)

Booking

Method Endpoint Description
POST /api/book-ticket Atomic booking (all-or-nothing)
GET /api/confirmation Booking confirmation + QR

Admin

Method Endpoint Description
GET/POST /api/admin/events List / create events
GET /api/admin/bookings All bookings with filters
GET /api/admin/analytics Revenue + occupancy KPIs
POST /api/admin/validate-ticket Check-in a QR ticket

Environment Variables

Variable Required Description
DB_HOST βœ… MySQL host
DB_USER βœ… MySQL user
DB_PASSWORD βœ… MySQL password
DB_NAME βœ… Database name
AUTH_SECRET βœ… JWT signing secret (openssl rand -base64 32)
SEAT_PRICE β€” Price config map (defaults in code)

Roadmap

Planned:

  • Ticket.used_at timestamp column β€” replace Transaction_Log lookup with a single column
  • Signed QR codes (HMAC over booking_id|seat_id|secret) for offline scanner verification
  • Refund flow + Payment.refunded_at
  • Per-event seat pricing stored in DB (currently a config map)
  • Rate limiting on /api/auth/login and /api/seats/lock

Deliberately not planned:

  • WebSockets for the seat map β€” polling is cheaper; the UX is identical at this scale
  • Separate service layer β€” lib/queries/ already serves this role cleanly
  • Redis for seat locks β€” MySQL row-level locks handle thousands of concurrent users; add Redis only when you have profiler evidence of contention

Contributing

  1. Fork the repo
  2. Create a feature branch: git checkout -b feat/your-feature
  3. Commit with conventional commits: git commit -m "feat: add offline QR validation"
  4. Open a pull request

Please run the load test before submitting changes to the booking or seat lock flow.


License

Yash Β© 2025 TicketFlow Contributors

About

Full-stack Ticket Booking System featuring dynamic seat availability, booking management, payment processing, and database-level concurrency control built with MySQL and modern frontend technologies.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages