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