Implementing Event Seat Booking on a Website
Event seat booking differs from other types: seats are fixed on a venue map, sales come in waves (early bird → general sale → last chance), and load can peak at sales start.
Data Model
CREATE TABLE events (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
venue_name VARCHAR(255),
venue_address TEXT,
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP,
sales_open_at TIMESTAMP,
sales_close_at TIMESTAMP,
status VARCHAR(20) DEFAULT 'draft', -- draft|on_sale|sold_out|finished
schema_id INTEGER, -- venue map
cover_image VARCHAR(500)
);
CREATE TABLE seat_categories (
id SERIAL PRIMARY KEY,
event_id INTEGER REFERENCES events(id),
name VARCHAR(100), -- 'Orchestra', 'Balcony', 'VIP'
color VARCHAR(7), -- color on venue map
price NUMERIC(10,2),
total_seats INTEGER,
available_seats INTEGER
);
CREATE TABLE seats (
id BIGSERIAL PRIMARY KEY,
event_id INTEGER REFERENCES events(id),
category_id INTEGER REFERENCES seat_categories(id),
row_label VARCHAR(10),
seat_number VARCHAR(10),
x_pos NUMERIC(6,2), -- coordinate on map
y_pos NUMERIC(6,2),
status VARCHAR(20) DEFAULT 'available',
-- available | held | sold | blocked | unavailable
booking_id BIGINT,
held_until TIMESTAMP,
held_by VARCHAR(100) -- session_id
);
CREATE TABLE ticket_bookings (
id BIGSERIAL PRIMARY KEY,
event_id INTEGER REFERENCES events(id),
customer_name VARCHAR(255),
customer_email VARCHAR(255),
customer_phone VARCHAR(50),
total_amount NUMERIC(10,2),
status VARCHAR(20) DEFAULT 'pending',
payment_id VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
paid_at TIMESTAMP,
cancelled_at TIMESTAMP
);
CREATE TABLE ticket_booking_seats (
booking_id BIGINT REFERENCES ticket_bookings(id),
seat_id BIGINT REFERENCES seats(id),
price NUMERIC(10,2),
PRIMARY KEY (booking_id, seat_id)
);
Seat Hold with TTL
When the client selects seats, they are temporarily blocked until payment completes or timer expires:
HOLD_TTL_SECONDS = 600 # 10 minutes
def hold_seats(seat_ids: list[int], session_id: str) -> bool:
with db.transaction():
# Atomically check and block
result = db.execute("""
UPDATE seats
SET status = 'held',
held_by = %(session)s,
held_until = NOW() + INTERVAL '10 minutes'
WHERE id = ANY(%(ids)s)
AND status = 'available'
RETURNING id
""", {'ids': seat_ids, 'session': session_id})
held_count = len(result)
if held_count < len(seat_ids):
# Not all seats available — rollback transaction
raise db.Rollback("Some seats are no longer available")
return True
Expired holds are released by a background process running every minute:
UPDATE seats
SET status = 'available', held_by = NULL, held_until = NULL
WHERE status = 'held' AND held_until < NOW();
Venue Map (Seat Map)
Venue layout is rendered on SVG or Canvas. Seat data is requested from backend:
{
"sections": [
{
"id": 1,
"name": "Orchestra",
"rows": [
{
"label": "A",
"seats": [
{ "id": 1001, "number": "1", "x": 100, "y": 200, "status": "available", "price": 2500 },
{ "id": 1002, "number": "2", "x": 130, "y": 200, "status": "sold", "price": 2500 }
]
}
]
}
]
}
User clicks a seat, it highlights, gets added to cart. On attempt to add already taken seat — error shows without page reload (WebSocket or polling every 5 sec).
Real-Time Updates
During high demand, seats are snapped up fast. Real-time schema updates via WebSocket:
const ws = new WebSocket(`wss://api.example.com/events/${eventId}/seats`);
ws.onmessage = (event) => {
const { seat_id, status } = JSON.parse(event.data);
updateSeatStatus(seat_id, status); // update color on map
};
Backend broadcasts updates to all connected clients on each seat status change.
Pricing Categories and Sales Waves
CREATE TABLE pricing_tiers (
id SERIAL PRIMARY KEY,
event_id INTEGER REFERENCES events(id),
name VARCHAR(100), -- 'Early Bird', 'Standard'
category_id INTEGER REFERENCES seat_categories(id),
price NUMERIC(10,2),
available_from TIMESTAMP,
available_until TIMESTAMP,
quota INTEGER -- seat limit at this price
);
System automatically switches between tiers on schedule.
E-Tickets
After payment, each ticket is generated as PDF with QR code:
import qrcode
from reportlab.pdfgen import canvas
def generate_ticket_pdf(ticket: dict) -> bytes:
qr = qrcode.make(ticket['verification_code'])
# ... overlay QR on PDF template
return pdf_bytes
QR contains a signed token with ticket_id and booking_id. At entry — scanned and verified via API.
Implementation Timeline
Without venue map, simple seat numbering, online payment — 10–12 business days. Full seat map (SVG), real-time updates, e-tickets with QR, pricing tiers, entry verification — 16–22 business days.







