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.
Order of assembly
Section titled “Order of assembly”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
What an event row carries
Section titled “What an event row carries”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.
Packages and slots
Section titled “Packages and slots”Inside events.create / events.update, for each artist:
- Insert the
artist_packagesrow — price, benefits,perUserLimit,isPresale,sessionType, and the personal-item options (personalItemEnabled,personalItemAddonPrice, and the two labels). - 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.
Publishing
Section titled “Publishing”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.faqUpdatePayloadSchemaenforces 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.
Who can do what
Section titled “Who can do what”| 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.
Checklist before opening sales
Section titled “Checklist before opening sales”- Event dates and
timezonecorrect — everything customer-facing renders inAsia/Kuala_Lumpur. - Entry ticket types created, names unique (enforced by
assertUniqueEntryTicketNames). - Artist packages priced,
perUserLimitset where it matters, presale flags final. FlippingisPresaleon a live package mid-checkout breaks every in-flight PaymentIntent — the amount no longer matches. - Time slots generated with the right
capacityandslotKind: 'signing'. - Booths laid out, timetable filled, gallery uploaded.
sessionGuidelinesSlugpoints 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.
