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
| 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 |
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.
- Node.js β₯ 18
- MySQL 8 database named
ticket_booking_system
# 1. Clone and install
git clone https://github.com/your-org/ticketflow.git
cd ticketflow
npm install
# 2. Configure environment
cp .env.example .env.localEdit .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 devOpen http://localhost:3000.
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';
SQLThe 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
β UNIQUE constraint enforced at DB level β the real safety net against double-bookings. i INDEX on hot query columns β matches every frequent read path.
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.
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.
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.
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
Ticketis the real safety net. Application-level locks are defence-in-depth; the DB constraint is what actually prevents double-bookings.
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
setIntervalfor a Server-Sent Events stream on the same endpoint
/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.
Admin scanner UI at /admin/scanner:
- Inspect β paste or scan a QR code to preview ticket details (event, seat, holder, payment status)
- Check In β mark the ticket used; recorded in
Transaction_Logwithaction_type = 'CHECKIN-{ticket_id}' - 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.
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
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.mjsSpins 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.
| 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 |
| 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) |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/book-ticket |
Atomic booking (all-or-nothing) |
GET |
/api/confirmation |
Booking confirmation + QR |
| 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 |
| 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) |
Planned:
-
Ticket.used_attimestamp 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/loginand/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
- Fork the repo
- Create a feature branch:
git checkout -b feat/your-feature - Commit with conventional commits:
git commit -m "feat: add offline QR validation" - Open a pull request
Please run the load test before submitting changes to the booking or seat lock flow.
Yash Β© 2025 TicketFlow Contributors