Skip to content

Event setup flow

Setting up an event is one nested write: the event, its entry ticket types, its artist packages, and the time slots under each package all land together.

Primary code: packages/core/src/server/trpc/routers/events.ts. Admin UI: /admin/events/new, /admin/events/[id]/edit, and the per-event booth, timetable and gallery pages.

flowchart LR
    E[Event<br/>status=draft] --> T[Entry ticket types]
    E --> A[Artist packages]
    A --> S[Time slots<br/>slot_kind=signing]
    E --> B[Booths]
    E --> G[Timetable + gallery]
    E --> P{Publish}
    P -->|changeStatus| L[status=published<br/>cache purged, paths revalidated]
    L -.->|edits need unpublish| E

events holds more than dates and a venue: slug (unique, generated), isFreeEntry, status, the FAQ payload (faqCategories, faqDefaultOpenIds, faqUrls, faqDocumentUrl), sessionGuidelinesSlug (a foreign key into legal_pages), imagery, and timezone, which defaults to Asia/Kuala_Lumpur.

Slug generation lowercases the name, strips anything that is not alphanumeric, whitespace or a hyphen, collapses whitespace to hyphens and trims stray hyphens. An empty result becomes event-<timestamp>, and a collision gets -1, -2, … until it is unique.

Session guidelines are validated on write: a slug that does not exist in legal_pages is a 400, not a dangling link on the live page.

Inside events.create / events.update, for each artist:

  1. Insert the artist_packages row — price, benefits, perUserLimit, isPresale, sessionType, and the personal-item options (personalItemEnabled, personalItemAddonPrice, and the two labels).
  2. Batch-insert that package’s time_slots: startTime, capacity, slotKind, displayLabel.

A mystery package is created inactive (isActive: false) regardless of what was submitted, so it cannot be booked before it is revealed.

time_slots.status is active or cancelled. Cancelling is a first-class operation (cancelTimeSlot, admin-only) with cancelledAt and cancelReason, because an artist pulling out of a session has downstream consequences for bookings that already exist.

changeStatus moves the event between draft, published and expired. It is deliberately narrow: it writes the status, audit-logs PUBLISH / UNPUBLISH, drops the public event cache keys from Redis, and revalidates /events and /events/<slug>.

Two deliberate exceptions, both modelled as narrow mutations that skip the published guard because they cannot touch slots:

  • updateFaq — FAQ categories and default-open ids only. The content team edits copy on live events. faqUpdatePayloadSchema enforces the icon allow-list, unique item ids, and that every default-open id exists.
  • patchEntryTicketFlags — entry ticket flags only.

togglePackageActive is the supported way to take a single package off sale without unpublishing the event.

Action Roles
list, getById admin, editor (plus the roles listed on the procedure)
create, update, changeStatus, updateFaq, togglePackageActive admin, editor
delete, cancelTimeSlot, getTimeSlotBookings admin

Every one of these mutations audit-logs. See Roles and access.

  • Event dates and timezone correct — everything customer-facing renders in Asia/Kuala_Lumpur.
  • Entry ticket types created, names unique (enforced by assertUniqueEntryTicketNames).
  • Artist packages priced, perUserLimit set where it matters, presale flags final. Flipping isPresale on a live package mid-checkout breaks every in-flight PaymentIntent — the amount no longer matches.
  • Time slots generated with the right capacity and slotKind: 'signing'.
  • Booths laid out, timetable filled, gallery uploaded.
  • sessionGuidelinesSlug points at a real legal page.
  • Publish, then confirm /events/<slug> renders — publishing purges the Redis cache and revalidates the paths, but a stale CDN edge is worth one look.