Events & Ticketing
Events with tiered, priced admission, a cart that sells several tickets in one transaction, and a booking record per ticket held. Where the base types fit cleanly, where the fit is loose, and why capacity is a number you check rather than a limit the platform holds.
The Project
- •The thing being sold is scheduled, with a start, an end and a venue.
- •Admission comes in tiers at different prices — general, VIP, student, early bird — and each tier has its own quantity.
- •A buyer routinely purchases several tickets in one transaction, sometimes for people who are not them.
- •The ticket a person holds needs to exist as a record of its own, because it is what gets scanned, transferred or refunded.
- •You need a public event page and an operator back office over the same data.
ℹ️ The exemplar behind this page
Architecture & Design
{
"name": "ticket_tier",
"namespace": "skillsummit",
"schema_type": "object",
"schema_content": {
"name": "ticket_tier",
"parent_type": "event",
"extends": "service:base_service",
"attributes": [
{"name": "tier_name", "type": "string", "mandatory": true},
{"name": "capacity", "type": "integer", "mandatory": true},
{"name": "remaining", "type": "integer"},
{"name": "perks", "type": "string"},
{"name": "is_sold_out", "type": "boolean"},
{"name": "primary_image", "type": "Image"}
]
}
}| Type in skillsummit | Extends | Notes |
|---|---|---|
| event | none | a plain tenant-parented record: slug, title, summary, start_date, end_date, venue, venue_address, category, speaker_name, speaker_bio, speaker_photo, max_attendees, is_visible |
| ticket_tier | service:base_service | parented to event; adds tier_name, capacity, remaining, perks and is_sold_out |
| booking | ticket:base_ticket | one held ticket; references Event as resource, TicketTier as primary_item and Attendee as actor |
| attendee | customer:base_customer | adds company, job_title, dietary_preferences, linkedin_url and t_shirt_size |
| cart | cart:base_cart | status of open, checked_out, abandoned or expired |
| cart_item | cart:base_cart_item | parented to cart |
| order | order:base_order | references Attendee as actor; inherits the totals and the pending / confirmed / fulfilled / cancelled / refunded status |
| order_item | order:base_order_item | parented to order |
| attachment | attachment:base_attachment | file records for event media |
⚠️ The booking type inherits a support-ticket lifecycle
The End-to-End Flow
- 1
Publish an event
An event record with is_visible controlling whether it reaches the public landing page, and one ticket_tier per admission level beneath it. Because tiers are parented to the event, listing an event's tiers is a parent-scoped read rather than a filter you have to remember to apply.
- 2
Browse
The public surface reads events and their tiers through the ordinary CRUD endpoints with cursor pagination — a limit and a cursor. price and currency come from base_service, so the tier renders with a price without any extra modelling.
- 3
Add tickets to a cart
cart / add_to_cart, once per tier and quantity. The first call materialises the cart and ensures the buyer has an attendee profile, which means a self-signup buyer's access policy has to allow that create — this is the usual reason a first checkout fails before Stripe is reached.
- 4
Check out
cart / checkout_cart converts the cart into an order and copies cart_item rows into order_item rows. The cart moves to checked_out; the order arrives at pending carrying source_cart_uuid.
- 5
Take payment
stripe_checkout / create_checkout_session hands the buyer to Stripe and records a checkout session. Note what is absent: skillsummit defines no type extending payment:base_payment, so there is no payment record with its own authorize / capture / refund lifecycle — the money is represented only by the checkout session and the order total. The bookstore exemplar does model a payment; this one chose not to.
- 6
Issue the tickets
One booking record per ticket, each referencing its Event, its TicketTier and its Attendee, and each carrying attendee_name and attendee_email so a ticket bought for someone else names them. Nothing creates these automatically from the order — see the next section.
- 7
Deliver and follow up
The email service sends the confirmation and the tickets, with template, sender and campaign as records and suppression handled for you. amazon_s3 holds any attached material.
ℹ️ Illegal transitions are refused
What You Get Without Building It
- •The whole basket-to-order path: cart materialisation, totals, conversion into an order, and line-item copying.
- •A priced, sellable tier type from base_service, and a held-ticket type with a state machine from base_ticket.
- •A three-level hierarchy — tenant, event, tier — expressed as parent types rather than foreign keys you police.
- •Attendee profiles tied to user accounts, so a returning attendee is a reference walk rather than an email match.
- •Stripe Checkout and SendGrid as keystore-held credentials rather than embedded SDKs.
- •File storage on your own bucket for speaker photos and event media.
- •A public landing page assembled from hero, catalog, detail, cart and order blocks, and an admin panel generated per type — events, attendees, bookings, tiers, carts and orders.
- •Metric cards and charts as records: active events, total bookings, total revenue and a revenue trend.
💡 Importing a service is not the same as using one
How to Implement in Supero
Model it
- 1
Build the schema
Describe the app in App Studio — it generates the schemas and a reviewable UI — or author it over MCP from your editor. Aim at the object model above: a type opts into a service by extending its base type. Schemas are live on the CRUD API the moment they upload — there is no separate build step to run.
- 2
Configure the services
Imported services arrive uncredentialed. Put Stripe’s keys in the keystore for checkout, plus email and SMS for ticket delivery and reminders — see Email & Notifications. Until a service is credentialed, its calls fail with a clear error rather than silently doing nothing.
What you write yourself
⚠️ Capacity is a number to check, not a limit the platform holds
| Decision | The state of it in the exemplar |
|---|---|
| Turning an order into tickets | Nothing creates a booking record per order_item. Fan-out from the paid order to the individual tickets is a workflow you write, and it is the step most likely to be forgotten in a demo that looked complete. |
| The ticket lifecycle | The inherited states are the helpdesk set — open, in_progress, on_hold, resolved, closed, reopened. Check-in, transfer and refund are yours to model, whether as your own field or a different base type. |
| A payment record | This exemplar has none. If you need refunds tracked with captured and refunded amounts, add a type extending payment:base_payment and import that service, as the bookstore exemplar does. |
| Waitlists | There is no waitlist type and no queue behaviour. It is a schema plus a workflow that runs when capacity frees up. |
| Tiered pricing over time | Early-bird expiry is a rule about dates, not a feature of base_service. Either separate tiers with their own active windows, or a workflow that flips status on a schedule. |
| Buyer permissions | add_to_cart creates the attendee profile on the buyer's behalf. A self-signup tenant_user with a default-deny policy is blocked there, and sees a permission error rather than a checkout page. |
| Refunds and cancellations | cancel_order is an operation, not a policy. Windows, partial refunds and what happens to the issued bookings are yours. |
Deployment
- 1
Preview while you iterate
A preview deploy answers at
https://preview-{domain}-{project}.supero.livefor a short lease while you build and test. Publish Your App covers how preview and live differ. - 2
Go Live
Go Live publishes to a permanent, Supero-hosted URL —
https://live-{domain}-{project}.supero.live— with no cloud account to connect and nothing to provision. It is asynchronous: you get a202and a poll URL, then the live address. The full contract is in Publish Your App. - 3
Watch the logs
When a deploy fails or the live app misbehaves, the **Cloud Deploy Logs** page in the dashboard shows the published service’s container logs — pick the Live lane to read the published app rather than the preview build (up to ~24 hours back, ~500 lines per request). For platform activity — requests, auth, errors across your domain —
GET /api/v1/logsis a filtered read, and a single request traces across services at/api/v1/logs/trace/{correlation_id}when you set your ownX-Correlation-ID. See Operations & Observability.
Final Thoughts
remaining when a ticket is bought, nothing sets is_sold_out when it reaches zero, and nothing refuses a checkout against a tier that has none left — two simultaneous buyers of the last seat will both succeed unless you have made them not. And turning a paid order into individual booking records is a fan-out you write too, the step most likely to be forgotten in a demo that looked complete.Where to Go Next
The mechanism
Transactional Services
The cart, order and ticket state machines, services/execute, and idempotency. Read it
The gaps
Workflows & Automation
Where capacity checks and order-to-ticket fan-out actually live. Read it
The model
Schema Design
Parent types, references and roles, and how extends is resolved — the tier-under-event hierarchy in detail. Read it
The money
Managed Integrations
Stripe Checkout and SendGrid as keystore credentials rather than code. Read it
On this page