A full-stack event ticket booking platform built with modern technologies, featuring real-time seat selection with advanced locking mechanisms to prevent overbooking.
- Overview
- Core Features
- Tech Stack
- Architecture
- Backend Details
- Database Schema
- API Endpoints
- Seat Locking Mechanism
- Frontend Features
- Getting Started
- Environment Variables
- Scripts
The Booking Platform is a comprehensive ticket booking system designed for events like concerts, movies, sports, theater, comedy shows, and conferences. It allows users to:
- 🔍 Browse and search events with advanced filters
- 🪑 Select seats in real-time with interactive seat maps
- 💳 Book tickets with secure payment processing
- 📱 Manage their bookings and profile
The platform includes role-based access for Users, Organizers, and Admins.
- JWT-based authentication with access and refresh tokens
- Role-based access control (USER, ORGANIZER, ADMIN)
- Secure password hashing with bcrypt
- HTTP-only cookie support for enhanced security
- Profile management and password change
- Create, update, publish, and cancel events
- Event categories: Movie, Concert, Sports, Theater, Comedy, Conference
- Event lifecycle:
DRAFT→PUBLISHED→COMPLETED/CANCELLED - Event filtering by category, city, date range, and price range
- Auto-generated seats based on venue sections
- Event images and banners support
- Interactive seat map with sections (VIP, Balcony, Ground Floor, etc.)
- Two-phase locking to prevent race conditions and overbooking
- 10-minute seat hold with extension capability
- Real-time availability updates
- Maximum 6 seats per user per event
- Multi-seat booking in a single transaction
- Booking status tracking:
PENDING→CONFIRMED/CANCELLED/EXPIRED - Automatic booking expiration for unpaid reservations
- Booking history with detailed seat information
- Cancellation support with seat release
- Review and rating system (1-5 stars) for events
- Coupon and discount codes (percentage or fixed amount)
- User notifications (booking confirmations, event reminders)
- Background jobs for cleanup and maintenance
- Payment gateway ready (Razorpay support)
- GST tax calculation (18%)
- Refund processing capability
- Multiple payment methods (card, UPI, netbanking)
| Technology | Purpose |
|---|---|
| Node.js + Express 5 | REST API server |
| TypeScript | Type-safe development |
| Prisma ORM | Database operations |
| PostgreSQL | Primary database |
| JWT | Authentication tokens |
| bcrypt.js | Password hashing |
| node-cron | Background scheduled jobs |
| Helmet | Security HTTP headers |
| Morgan | HTTP request logging |
| Cookie Parser | HTTP-only cookie handling |
| Technology | Purpose |
|---|---|
| React 19 | UI framework |
| TypeScript | Type-safe development |
| Vite | Build tool and dev server |
| React Router DOM 7 | Client-side routing |
| Tailwind CSS 4 | Utility-first styling |
| Axios | HTTP client with interceptors |
BookingPlatform/
├── backend/
│ ├── src/
│ │ ├── config/ # Configuration files
│ │ │ ├── index.ts # Environment & app config
│ │ │ └── database.ts # Prisma client singleton
│ │ │
│ │ ├── middleware/ # Express middlewares
│ │ │ ├── auth.middleware.ts # JWT authentication
│ │ │ ├── error.middleware.ts # Global error handler
│ │ │ └── index.ts # Exports
│ │ │
│ │ ├── routes/ # API route handlers
│ │ │ ├── auth.routes.ts # /api/auth/*
│ │ │ ├── user.routes.ts # /api/users/*
│ │ │ ├── event.routes.ts # /api/events/*
│ │ │ ├── booking.routes.ts # /api/bookings/*
│ │ │ ├── venue.routes.ts # /api/venues/*
│ │ │ └── index.ts # Route aggregation
│ │ │
│ │ ├── services/ # Business logic layer
│ │ │ ├── auth.service.ts # Authentication logic
│ │ │ ├── event.service.ts # Event CRUD operations
│ │ │ ├── booking.service.ts # Booking management
│ │ │ ├── seatLock.service.ts # ⭐ Seat locking logic
│ │ │ └── index.ts # Exports
│ │ │
│ │ ├── jobs/ # Background jobs
│ │ │ └── cron.ts # Scheduled tasks
│ │ │
│ │ ├── types/ # TypeScript type definitions
│ │ ├── utils/ # Utility functions
│ │ └── index.ts # App entry point
│ │
│ └── prisma/
│ └── schema.prisma # Database schema
│
└── frontend/
└── src/
├── api/ # Axios API clients
│ ├── axios.ts # Axios instance config
│ ├── auth.api.ts # Auth endpoints
│ ├── event.api.ts # Event endpoints
│ ├── booking.api.ts # Booking endpoints
│ └── user.api.ts # User endpoints
│
├── components/ # Reusable React components
│ ├── booking/ # Seat grid, booking summary
│ ├── layout/ # Navbar, Footer
│ ├── events/ # Event filters
│ └── common/ # Shared components
│
├── context/ # React Context providers
│ └── AuthContext.tsx # Authentication state
│
├── pages/ # Application pages
│ ├── Home.tsx
│ ├── Events.tsx
│ ├── EventDetails.tsx
│ ├── BookingPage.tsx
│ ├── PaymentPage.tsx
│ ├── MyBookings.tsx
│ ├── Profile.tsx
│ ├── Login.tsx
│ ├── Register.tsx
│ └── organizer/ # Organizer dashboard
│ ├── OrganizerDashboard.tsx
│ ├── CreateEvent.tsx
│ └── EditEvent.tsx
│
├── types/ # TypeScript type definitions
├── utils/ # Utility functions
├── routes.tsx # Route configuration
└── App.tsx # Root component
The backend follows a layered architecture:
Request → Routes → Services → Prisma (Database)
↓
Response with JSON
The Express application initializes with:
- Security: Helmet for HTTP headers, CORS for cross-origin requests
- Middleware: Cookie parser, JSON body parser, Morgan logger
- Routes: All API endpoints mounted under
/api - Error Handling: Custom 404 and global error handlers
- Background Jobs: Cron jobs for expired holds and bookings
// Middleware stack
app.use(helmet()); // Security headers
app.use(cors({ credentials: true })); // CORS with cookies
app.use(cookieParser()); // Parse cookies
app.use(express.json()); // Parse JSON body
app.use(morgan('dev')); // Request logging
// Routes
app.use('/api', routes);
// Error handling
app.use(notFound);
app.use(errorHandler);Centralized configuration for:
export const config = {
// Server
port: 3001,
nodeEnv: 'development',
// JWT
jwt: {
secret: 'your-super-secret-key',
expiresIn: '7d',
refreshExpiresIn: '30d',
},
// Seat Hold Configuration
seatHold: {
ttlSeconds: 600, // 10 minutes
maxSeatsPerUser: 6, // Max seats per booking
},
// Pagination
pagination: {
defaultLimit: 10,
maxLimit: 100,
},
};Handles user authentication:
| Method | Description |
|---|---|
register() |
Create new user with hashed password |
login() |
Validate credentials and return JWT tokens |
refreshToken() |
Issue new access token from refresh token |
getProfile() |
Get user profile with booking count |
updateProfile() |
Update name and phone |
changePassword() |
Change password with current password verification |
Manages event lifecycle:
| Method | Description |
|---|---|
createEvent() |
Create event with auto-generated seats |
getEvents() |
Paginated listing with filters |
getEvent() |
Single event with venue and ratings |
getSeatAvailability() |
Seats grouped by section |
updateEvent() |
Update event details |
publishEvent() |
Change status to PUBLISHED |
cancelEvent() |
Cancel event and all bookings |
Handles the booking flow:
| Method | Description |
|---|---|
createBooking() |
Validate held seats and create PENDING booking |
confirmBooking() |
Confirm payment with optimistic locking |
cancelBooking() |
Cancel and release seats |
getBooking() |
Get booking with full details |
getUserBookings() |
Paginated user booking history |
expirePendingBookings() |
Background job for expired bookings |
Critical service for preventing overbooking:
| Method | Description |
|---|---|
holdSeats() |
Pessimistic lock on seats with TTL |
releaseSeats() |
Manual release of held seats |
extendHold() |
Extend hold time up to 10 minutes |
releaseExpiredHolds() |
Background cleanup job |
getHoldStatus() |
Check user's current holds |
Three scheduled tasks running continuously:
| Job | Schedule | Purpose |
|---|---|---|
| Release Expired Holds | Every minute | Free seats whose hold expired |
| Expire Pending Bookings | Every minute | Mark unpaid bookings as EXPIRED |
| Clean Old Notifications | Daily at midnight | Delete 30+ day old read notifications |
// Release expired seat holds
cron.schedule('* * * * *', async () => {
const result = await SeatLockService.releaseExpiredHolds();
if (result.released > 0) {
console.log(`Released ${result.released} expired seat holds`);
}
});┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │ │ Venue │ │ Coupon │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ id │ │ id │ │ id │
│ email │ │ name │ │ code │
│ password │ │ address │ │ discountType│
│ name │ │ city │ │ discountValue│
│ phone │ │ capacity │ │ validFrom │
│ role │ │ amenities[] │ │ validUntil │
└──────┬──────┘ └──────┬──────┘ └─────────────┘
│ │
│ ┌──────┴──────┐
│ │ Section │
│ ├─────────────┤
│ │ id │
│ │ name │
│ │ rowCount │
│ │ seatsPerRow │
│ │ priceMultiplier│
│ └──────┬──────┘
│ │
│ ┌──────┴──────┐ ┌─────────────┐
│ │ Event │───────│ Review │
│ ├─────────────┤ ├─────────────┤
│ │ id │ │ id │
│ │ title │ │ userId │
│ │ description │ │ eventId │
│ │ category │ │ rating (1-5)│
│ │ eventDate │ │ comment │
│ │ basePrice │ └─────────────┘
│ │ status │
│ └──────┬──────┘
│ │
│ ┌──────┴──────┐
│ │ Seat │
│ ├─────────────┤
│ │ id │
│ │ eventId │
│ │ sectionId │
│ │ rowNumber │
│ │ seatNumber │
│ │ price │
│ │ status │ ← AVAILABLE|HELD|BOOKED|BLOCKED
│ │ heldBy │ ← User ID
│ │ heldUntil │ ← Expiry timestamp
│ │ version │ ← Optimistic lock
│ └──────┬──────┘
│ │
┌──────┴──────┐ ┌──────┴──────┐ ┌─────────────┐
│ Booking │───────│ BookingItem │ │ Notification│
├─────────────┤ ├─────────────┤ ├─────────────┤
│ id │ │ id │ │ id │
│ bookingNumber│ │ bookingId │ │ userId │
│ userId │ │ seatId │ │ type │
│ eventId │ │ price │ │ title │
│ status │ └─────────────┘ │ message │
│ totalAmount │ │ isRead │
│ taxAmount │ └─────────────┘
│ finalAmount │
│ expiresAt │
│ version │ ← Optimistic lock
└──────┬──────┘
│
┌──────┴──────┐
│ Payment │
├─────────────┤
│ id │
│ bookingId │
│ gatewayOrderId│
│ gatewayPaymentId│
│ amount │
│ status │
│ method │
└─────────────┘
enum UserRole {
USER
ADMIN
ORGANIZER
}
enum EventCategory {
MOVIE
CONCERT
SPORTS
THEATER
COMEDY
CONFERENCE
OTHER
}
enum EventStatus {
DRAFT
PUBLISHED
CANCELLED
COMPLETED
}
enum SeatStatus {
AVAILABLE
HELD
BOOKED
BLOCKED
}
enum BookingStatus {
PENDING
CONFIRMED
CANCELLED
REFUNDED
EXPIRED
}
enum PaymentStatus {
PENDING
PROCESSING
COMPLETED
FAILED
REFUNDED
PARTIALLY_REFUNDED
}| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
❌ | Register new user |
| POST | /api/auth/login |
❌ | Login and get tokens |
| POST | /api/auth/refresh-token |
❌ | Refresh access token |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/users/profile |
✅ | Get current user profile |
| PUT | /api/users/profile |
✅ | Update profile |
| PUT | /api/users/change-password |
✅ | Change password |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/events |
❌ | List events with filters |
| GET | /api/events/:id |
❌ | Get event details |
| GET | /api/events/:id/seats |
❌ | Get seat availability |
| POST | /api/events |
✅ Admin | Create event |
| PUT | /api/events/:id |
✅ Admin | Update event |
| POST | /api/events/:id/publish |
✅ Admin | Publish event |
| POST | /api/events/:id/cancel |
✅ Admin | Cancel event |
Query Parameters for GET /api/events:
category- Filter by event categorycity- Filter by venue citydateFrom- Events starting after this datedateTo- Events starting before this dateminPrice- Minimum base pricemaxPrice- Maximum base pricestatus- Filter by status (all, DRAFT, PUBLISHED, etc.)page- Page number (default: 1)limit- Items per page (default: 10)
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/bookings/hold-seats |
✅ | Hold/lock seats |
| POST | /api/bookings/release-seats |
✅ | Release held seats |
| POST | /api/bookings/extend-hold |
✅ | Extend hold time |
| GET | /api/bookings/hold-status |
✅ | Get held seats |
| POST | /api/bookings/create |
✅ | Create booking |
| POST | /api/bookings/:id/confirm |
✅ | Confirm after payment |
| POST | /api/bookings/:id/cancel |
✅ | Cancel booking |
| GET | /api/bookings/:id |
✅ | Get booking details |
| GET | /api/bookings |
✅ | List user bookings |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/venues |
❌ | List all venues |
| GET | /api/venues/:id |
❌ | Get venue details |
| POST | /api/venues |
✅ Admin | Create venue |
| PUT | /api/venues/:id |
✅ Admin | Update venue |
| POST | /api/venues/:id/sections |
✅ Admin | Add section |
The platform uses a two-phase locking strategy to prevent overbooking in high-concurrency scenarios:
When a user selects seats, we use database transactions with strict isolation:
// In SeatLockService.holdSeats()
const result = await prisma.$transaction(async (tx) => {
// 1. Check user's existing holds (max 6 seats total)
const existingHolds = await tx.seat.count({
where: {
eventId,
heldBy: userId,
status: SeatStatus.HELD,
heldUntil: { gt: new Date() }
}
});
if (existingHolds + seatIds.length > 6) {
throw new ApiError(400, 'Cannot hold more than 6 seats');
}
// 2. Find and verify seats are available
const availableSeats = await tx.seat.findMany({
where: {
id: { in: seatIds },
eventId,
status: SeatStatus.AVAILABLE
}
});
if (availableSeats.length !== seatIds.length) {
throw new ApiError(409, 'Some seats are no longer available');
}
// 3. Update seats to HELD (race condition protected by WHERE clause)
const updateResult = await tx.seat.updateMany({
where: {
id: { in: seatIds },
status: SeatStatus.AVAILABLE // ← Only if still available!
},
data: {
status: SeatStatus.HELD,
heldBy: userId,
heldUntil: new Date(Date.now() + 10 * 60 * 1000), // 10 min
version: { increment: 1 }
}
});
// 4. Verify all seats were updated
if (updateResult.count !== seatIds.length) {
throw new ApiError(409, 'Some seats were booked by another user');
}
return { success: true };
}, {
timeout: 10000 // 10 second timeout
});Key Protections:
- Transaction isolation prevents concurrent modifications
- WHERE clause includes
status: AVAILABLEto prevent double-booking - Count verification ensures all seats were successfully locked
- Timeout prevents indefinite locking
When confirming payment, we use version checking:
// In BookingService.confirmBooking()
const updatedBooking = await tx.booking.updateMany({
where: {
id: bookingId,
version: booking.version, // ← Optimistic lock check
status: BookingStatus.PENDING
},
data: {
status: BookingStatus.CONFIRMED,
paymentId,
confirmedAt: new Date(),
version: { increment: 1 } // ← Increment version
}
});
if (updatedBooking.count === 0) {
throw new ApiError(409, 'Booking was modified by another transaction');
}┌─────────────────────────────────────────────────────────────────────┐
│ STEP 1: User selects seats │
│ ───────────────────────────────────────────────────────────────── │
│ POST /api/bookings/hold-seats │
│ Body: { eventId, seatIds: ["seat-1", "seat-2"] } │
│ │
│ ✓ Pessimistic lock acquired │
│ ✓ Seats: AVAILABLE → HELD │
│ ✓ TTL: 10 minutes from now │
│ Response: { success: true, expiresAt: "2024-01-15T12:10:00Z" } │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 2: User creates booking │
│ ───────────────────────────────────────────────────────────────── │
│ POST /api/bookings/create │
│ Body: { eventId, seatIds: ["seat-1", "seat-2"] } │
│ │
│ ✓ Validates seats are still held by this user │
│ ✓ Creates PENDING booking with pricing │
│ ✓ Calculates: totalAmount + 18% GST = finalAmount │
│ Response: { bookingId, status: "PENDING", finalAmount: 1180 } │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 3: User completes payment │
│ ───────────────────────────────────────────────────────────────── │
│ POST /api/bookings/:bookingId/confirm │
│ Body: { paymentId: "pay_xyz", paymentMethod: "card" } │
│ │
│ ✓ Optimistic lock check on booking version │
│ ✓ Booking: PENDING → CONFIRMED │
│ ✓ Seats: HELD → BOOKED │
│ ✓ Payment record created │
│ Response: { status: "CONFIRMED", confirmedAt: "..." } │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ BACKGROUND: Cleanup jobs (every minute) │
│ ───────────────────────────────────────────────────────────────── │
│ • Releases expired seat holds (heldUntil < now) │
│ • Expires PENDING bookings past their expiresAt │
│ • Frees seats for other users to book │
└─────────────────────────────────────────────────────────────────────┘
| Scenario | Protection | Result |
|---|---|---|
| Two users click same seat simultaneously | Pessimistic lock + WHERE clause | Only one succeeds |
| User's hold expires during payment | Booking expiry check | Payment rejected |
| Network retry during confirmation | Optimistic lock version check | Duplicate rejected |
| User tries to hold 7+ seats | Max seats validation | Request rejected |
| Page | Route | Auth | Description |
|---|---|---|---|
| Home | / |
❌ | Landing page with featured events |
| Events | /events |
❌ | Browsable event list with filters |
| Event Details | /events/:id |
❌ | Event info, venue, reviews |
| Booking | /booking/:eventId |
✅ | Interactive seat selection |
| Payment | /payment/:bookingId |
✅ | Payment form and confirmation |
| My Bookings | /my-bookings |
✅ | User's booking history |
| Profile | /profile |
✅ | User profile management |
| Login | /login |
❌ | Authentication |
| Register | /register |
❌ | New user registration |
| Organizer Dashboard | /organizer/dashboard |
✅ Organizer | Event management |
| Create Event | /organizer/events/create |
✅ Organizer | New event form |
| Edit Event | /organizer/events/:id/edit |
✅ Organizer | Edit existing event |
| Component | Description |
|---|---|
| SeatGrid | Interactive seat map with real-time status updates |
| EventFilters | Category, date, price, and city filtering |
| Navbar | Navigation with auth state and avatar dropdown |
| BookingSummary | Price breakdown with taxes |
| EventCard | Event preview in listings |
- AuthContext: Global authentication state using React Context
- Local State: Component-level state with useState/useEffect
- URL State: Filters and pagination via query parameters
- Node.js 18 or higher
- PostgreSQL 14 or higher
- npm or yarn
git clone https://github.com/yourusername/booking-platform.git
cd booking-platformcd backend
# Install dependencies
npm install
# Set up environment variables
cp .env.example .env
# Edit .env with your database credentials (see below)
# Generate Prisma client
npm run db:generate
# Run database migrations
npm run db:migrate
# (Optional) Seed the database
npm run db:seed
# Start development server
npm run devThe backend will run on http://localhost:3001
cd frontend
# Install dependencies
npm install
# Set up environment variables
echo "VITE_API_URL=http://localhost:3001" > .env
# Start development server
npm run devThe frontend will run on http://localhost:5173
# Database
DATABASE_URL=postgresql://username:password@localhost:5432/bookingplatform
# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_EXPIRES_IN=7d
JWT_REFRESH_EXPIRES_IN=30d
# Server
PORT=3001
NODE_ENV=development
# Seat Hold Configuration
SEAT_HOLD_TTL=600 # 10 minutes in seconds
MAX_SEATS_PER_USER=6
# CORS
CORS_ORIGIN=http://localhost:5173
# Payment Gateway (Optional)
PAYMENT_GATEWAY=razorpay
RAZORPAY_KEY_ID=your_razorpay_key
RAZORPAY_KEY_SECRET=your_razorpay_secret
# Redis (Optional - for distributed deployments)
REDIS_HOST=localhost
REDIS_PORT=6379VITE_API_URL=http://localhost:3001| Script | Description |
|---|---|
npm run dev |
Start dev server with hot reload (nodemon) |
npm run build |
Compile TypeScript to JavaScript |
npm start |
Start production server |
npm run db:generate |
Generate Prisma client |
npm run db:migrate |
Run pending migrations |
npm run db:push |
Push schema changes (dev only) |
npm run db:seed |
Seed database with sample data |
npm run db:studio |
Open Prisma Studio GUI |
| Script | Description |
|---|---|
npm run dev |
Start Vite dev server with HMR |
npm run build |
Build production bundle |
npm run preview |
Preview production build locally |
npm run lint |
Run ESLint |
- Set environment variables in your hosting platform
- Build command:
npm run build - Start command:
npm start - Ensure PostgreSQL database is accessible
- Set
VITE_API_URLto your backend URL - Build command:
npm run build - Output directory:
dist - Configure
vercel.jsonfor SPA routing:
{
"rewrites": [
{ "source": "/(.*)", "destination": "/" }
]
}- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the ISC License.
Built by Sahil Agarwal