Skip to main content

Overview

EventPalour’s ticketing system supports multiple ticket types per event with flexible pricing, availability controls, and comprehensive status tracking. The system handles both ticket definitions and purchased ticket instances.

Ticket Architecture

The ticketing system consists of three main components:
  1. Ticket Types: Reusable ticket type definitions (e.g., “VIP”, “Early Bird”)
  2. Tickets: Event-specific ticket configurations with pricing and availability
  3. Purchased Tickets: Individual ticket instances owned by users

Ticket Types

Ticket types are workspace-level templates defined in tickets_types table:
{
  id: varchar(16),              // Ticket type ID
  workspace_id: varchar(16),    // Owner workspace
  name: varchar(255),           // Type name (e.g., "VIP", "Early Bird", "Standard")
  created_at: timestamp,
  updated_at: timestamp
}
Ticket types are reusable across all events in a workspace. Create types once and apply them to multiple events.

Common Ticket Types

VIP

Premium access with exclusive benefits

Early Bird

Discounted tickets for early registrations

Standard

Regular admission tickets

Student

Discounted tickets for students

Tickets Schema

The tickets table defines event-specific ticket configurations:
FieldTypeDescription
idvarchar(16)Unique ticket ID
event_idvarchar(16)Associated event (foreign key)
ticket_type_idvarchar(16)Ticket type (foreign key)
pricenumeric(10, 2)Ticket price
availability_quantityintegerMax tickets (NULL = unlimited)
currencycurrency_enumPrice currency (default: “KES”)
valid_fromtimestampSales start time (with timezone)
valid_untiltimestampSales end time (with timezone)
created_attimestampCreation timestamp
updated_attimestampLast update timestamp
Tickets connect event-specific configurations (price, availability) with workspace-level ticket types (name, category).

Currency Support

Currently supported currencies:
// From /lib/db/schema/enums.ts:85-88
enum CurrencyEnum {
  USD = "USD",
  KES = "KES"   // Kenyan Shilling (default)
}

Availability Management

Unlimited Tickets

Set availability_quantity to NULL for unlimited ticket sales:
const unlimitedTicket = {
  event_id: "evt123",
  ticket_type_id: "standard",
  price: 1000.00,
  availability_quantity: null,  // Unlimited
  currency: "KES"
};

Limited Tickets

Set a specific number for capacity-limited events:
const limitedTicket = {
  event_id: "evt123",
  ticket_type_id: "vip",
  price: 5000.00,
  availability_quantity: 50,    // Only 50 VIP tickets
  currency: "KES"
};
Always check available quantity before allowing purchases. Implement race condition protection for high-demand events.

Time-based Availability

Control when tickets can be purchased using valid_from and valid_until:
const earlyBirdTicket = {
  ticket_type_id: "early_bird",
  price: 800.00,
  valid_from: new Date('2024-01-01T00:00:00Z'),
  valid_until: new Date('2024-01-31T23:59:59Z')  // Available only in January
};

Purchased Tickets

When users buy tickets, instances are created in purchased_tickets:
FieldTypeDescription
idvarchar(16)Purchased ticket ID
user_idvarchar(16)Ticket owner
ticket_idvarchar(16)Reference to ticket definition
statusticket_status_enumCurrent status (default: “sold”)
pricenumeric(10, 2)Actual purchase price
quantityintegerNumber of tickets purchased
usedbooleanWhether ticket was scanned (default: false)
used_attimestampScan/redemption timestamp
created_attimestampPurchase timestamp
updated_attimestampLast update timestamp

Ticket Status Lifecycle

Purchased tickets transition through various statuses:

Status Definitions

// From /lib/db/schema/enums.ts:42-52
enum TicketStatus {
  AVAILABLE = "available",      // Ready for purchase
  RESERVED = "reserved",        // Temporarily held during checkout
  SOLD = "sold",                // Successfully purchased
  CANCELLED = "cancelled",      // Cancelled by user
  REFUNDED = "refunded",        // Refund processed
  TRANSFERRED = "transferred",  // Transferred to another user
  USED = "used",                // Scanned/redeemed at event
  EXPIRED = "expired",          // Past validity period
  ON_HOLD = "on_hold"          // Administrative hold
}
Ticket is ready for purchase. Default state for ticket inventory.

Ticket Reservations

The ticket_reservations table manages temporary holds during checkout:
{
  id: varchar(16),              // Reservation ID
  ticket_id: varchar(16),       // Reserved ticket
  user_id: varchar(16),         // User holding reservation
  quantity: integer,            // Number reserved (default: 1)
  expires_at: timestamp,        // Expiration time (with timezone)
  created_at: timestamp         // When reservation was made
}
1

Create Reservation

When user begins checkout, create reservation to hold tickets.
2

Set Expiration

Set expires_at to 10-15 minutes from creation.
3

Complete Purchase

If payment succeeds, create purchased_tickets and delete reservation.
4

Handle Expiration

If payment times out, delete expired reservation to release tickets.
Implement a background job to clean up expired reservations and return tickets to available inventory.

Ticket Relationships

Ticket Transfers

Users can transfer tickets to others via ticket_transfers:
{
  id: varchar(16),
  purchased_ticket_id: varchar(16),     // Which ticket is being transferred
  from_user_id: varchar(16),            // Current owner
  to_user_id: varchar(16),              // New owner
  transferred_at: timestamp             // Transfer timestamp
}
When a transfer completes, update the purchased_tickets.user_id to the new owner and set status to TRANSFERRED.

Ticket Scanning

Track ticket redemption at event entrances using ticket_scans:
{
  id: varchar(16),
  purchased_ticket_id: varchar(16),     // Which ticket was scanned
  scanned_by_user_id: varchar(16),      // Staff member who scanned (optional)
  scanned_at: timestamp                 // Scan timestamp
}
The table has a unique constraint on purchased_ticket_id to prevent double-scanning. Each ticket can only be scanned once.

Pricing Strategies

Tiered Pricing

Create multiple tickets for the same event with different prices:
const tickets = [
  {
    ticket_type_id: "early_bird",
    price: 500.00,
    availability_quantity: 100,
    valid_from: '2024-01-01',
    valid_until: '2024-01-31'
  },
  {
    ticket_type_id: "standard",
    price: 800.00,
    availability_quantity: 200,
    valid_from: '2024-02-01',
    valid_until: '2024-03-15'
  },
  {
    ticket_type_id: "vip",
    price: 2000.00,
    availability_quantity: 50,
    valid_from: '2024-01-01',
    valid_until: '2024-03-15'
  }
];

Dynamic Pricing

Adjust prices based on demand by creating new ticket records:
  1. Set valid_until on current tickets to end availability
  2. Create new tickets with updated prices
  3. Set valid_from to when new pricing takes effect

Best Practices

  • Use database transactions when decrementing availability_quantity
  • Implement optimistic locking to prevent overselling
  • Monitor inventory levels and alert when running low
  • Set reasonable expiration times (10-15 minutes)
  • Run cleanup jobs every 1-2 minutes to release expired reservations
  • Notify users before their reservation expires
Validate status changes:
  • RESERVEDSOLD (only if payment succeeds)
  • SOLDUSED (only at event time, with proper authentication)
  • SOLDREFUNDED (only within refund policy window)
Store the actual purchase price in purchased_tickets.price. Never reference the current ticket price for historical purchases.
  • Always specify currency when displaying prices
  • Store all prices as decimal(10, 2) for precision
  • Handle currency conversions at application level if needed

Common Queries

-- Get available tickets for an event
SELECT 
  t.*,
  tt.name as ticket_type_name,
  COALESCE(t.availability_quantity - COUNT(pt.id), t.availability_quantity) as remaining
FROM tickets t
JOIN tickets_types tt ON t.ticket_type_id = tt.id
LEFT JOIN purchased_tickets pt ON t.id = pt.ticket_id 
  AND pt.status IN ('sold', 'reserved')
WHERE t.event_id = 'evt123'
  AND (t.valid_from IS NULL OR t.valid_from <= NOW())
  AND (t.valid_until IS NULL OR t.valid_until >= NOW())
GROUP BY t.id, tt.name;

Technical Details

Schema Location

/lib/db/schema/tickets.ts
/lib/db/schema/ticket_reservations.ts
/lib/db/schema/attendee.ts (purchased_tickets)
/lib/db/schema/enums.ts

Key Relationships

Defined in /lib/db/schema/relations.ts:95-119, 254-265:
  • Tickets belong to events and ticket types
  • Purchased tickets belong to users and reference tickets
  • Reservations temporarily hold tickets for users
  • Transfers track ownership changes

Build docs developers (and LLMs) love