S
supero.docs
Documentation/Application Architectures/Events & Ticketing
Solution Architecture

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.

An events application with tiered tickets sold through a cart and recorded as bookings
Publish this elsewhere:

The Project

Ticketing sits between a storefront and a booking application, and takes something from each. Like a storefront, it sells several items in one basket to a buyer who may be buying for other people. Like a booking application, what it sells is a finite quantity attached to a moment in time. The unusual part is the middle layer: a customer does not buy an event, they buy a tier of admission to it, and the tiers differ in price, in perks and in how many exist.
Reach for this shape when the following hold. Conferences, workshops, training courses, performances and paid webinars all land here.
  • •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

This page follows bundle_m70-skillsummit, a generated workshop and conference ticketing application in the namespace skillsummit. It carries 29 schemas and imports thirteen services. It is the most interesting of the commerce exemplars because the fit between its domain and the available base types is imperfect in a way the others are not, and that is worth seeing rather than hiding.

Architecture & Design

The design turns on one line — a type earns its behaviour by extending a base type — and ticketing is where the fit strains.
Three layers: the event, the tiers of admission beneath it, and the tickets people hold. Only the middle and bottom layers extend anything.
The tier is the interesting declaration. It extends service:base_service — the same base type a salon uses for a haircut — because that is what carries price, currency and a sellable status. And its parent_type is event rather than tenant, so a tier lives underneath its event in the hierarchy and its fq_name reflects that.
json
{
  "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"}
    ]
  }
}
Six declared fields, plus everything base_service contributes: price, currency, duration_minutes, pricing_unit, and a status of active, inactive or discontinued. The full model:
Type in skillsummitExtendsNotes
eventnonea 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_tierservice:base_serviceparented to event; adds tier_name, capacity, remaining, perks and is_sold_out
bookingticket:base_ticketone held ticket; references Event as resource, TicketTier as primary_item and Attendee as actor
attendeecustomer:base_customeradds company, job_title, dietary_preferences, linkedin_url and t_shirt_size
cartcart:base_cartstatus of open, checked_out, abandoned or expired
cart_itemcart:base_cart_itemparented to cart
orderorder:base_orderreferences Attendee as actor; inherits the totals and the pending / confirmed / fulfilled / cancelled / refunded status
order_itemorder:base_order_itemparented to order
attachmentattachment:base_attachmentfile records for event media

⚠️ The booking type inherits a support-ticket lifecycle

ticket:base_ticket is the platform's helpdesk base type, not an admission-ticket type. Extending it gives skillsummit's booking a mandatory subject field and a status vocabulary of open, in_progress, on_hold, resolved, closed and reopened, driven by operations named assign_ticket, resolve_ticket and close_ticket. That is a workable container for a held ticket, and it is what the generator chose — but issued, checked_in, transferred and refunded are not among those states. If your ticket lifecycle matters, either add your own status field alongside the inherited one and drive it yourself, or extend a base type whose state machine you actually want. Read the inherited vocabulary before you build a scanner against it.
Everything else in the bundle came in with a service: event_category is a plain value list, the email service brings campaign, template, sender, message and suppression, Stripe Checkout brings session and payment_link, and the workflow engine brings its three record types.

The End-to-End Flow

The purchase path is the storefront path with tiers standing in for products. Every transactional step is one call to POST /api/v1/services/execute naming a service_id, an operation and the extending schema.
  1. 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. 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. 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. 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. 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. 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. 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

The cart, order and ticket services all ship state machines, and an operation from the wrong state returns HTTP 409 with INVALID_STATE_TRANSITION rather than being applied. Repeating an operation that already succeeded is idempotent and preserves the original timestamp. Transactional Services has the wire format.

What You Get Without Building It

SkillSummit imports thirteen services: cart, order, ticket, service, product, booking, customer, attachment, email, amazon_s3, stripe_checkout, ai and workflows. What that actually delivers:
  • •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

Two entries in that list of thirteen have nothing extending them: no schema in skillsummit extends booking:base_booking or product:base_product. The services are present and idle. This is worth internalising because it is the whole model in one observation — a service acts on your data only through a schema that extends its base type, and importing it does nothing on its own.
The bundle also imports the ai service. That is the catalog integration — a credential you supply, held in the keystore, for calls your own application code makes. It is not the same thing as the AI Data Agent, which does let your end users ask questions of their records in plain language and runs on the platform's own model. Two different capabilities that share a word; see that page for which is which.

How to Implement in Supero

Two kinds of work: first you model the shape above and point each type at its base type, then you build the parts the base types deliberately leave to you.

Model it

  1. 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. 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

Ticketing is the shape where the gap between a field and a guarantee costs the most, because the failure mode is selling something that does not exist.

⚠️ Capacity is a number to check, not a limit the platform holds

capacity is mandatory on a tier, and remaining, is_sold_out and the event's max_attendees sit alongside it. All four are ordinary fields. Nothing decrements 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. The platform takes no lock on a tier. Overselling is prevented by logic you write — a workflow bound to the checkout event that re-reads the tier and reverses the order, or a claim-then-confirm step in front of the cart. Two simultaneous buyers of the last seat will both succeed unless you have made them not.
DecisionThe state of it in the exemplar
Turning an order into ticketsNothing 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 lifecycleThe 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 recordThis 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.
WaitlistsThere is no waitlist type and no queue behaviour. It is a schema plus a workflow that runs when capacity frees up.
Tiered pricing over timeEarly-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 permissionsadd_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 cancellationscancel_order is an operation, not a policy. Windows, partial refunds and what happens to the issued bookings are yours.
The natural home for all of this is the workflow engine. A workflow is a record of type wf:workflow_definition, its steps are service_call, crud_operation, condition, parallel or transform, and an event binding fires it on a CRUD event — creation of an order, for instance — with an input map from event fields to workflow inputs. Errors are per-step: stop, continue or retry, with a retry count, a retry delay and a timeout. Every run leaves a workflow_instance and one workflow_step_log per step, so a ticket fan-out that failed on the fourth seat is diagnosable rather than mysterious. See Workflows & Automation and the Workflow Engine Reference.

Deployment

The path from this shape to a running ticketing app at a public URL, end to end:
  1. 1

    Preview while you iterate

    A preview deploy answers at https://preview-{domain}-{project}.supero.live for a short lease while you build and test. Publish Your App covers how preview and live differ.

  2. 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 a 202 and a poll URL, then the live address. The full contract is in Publish Your App.

  3. 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/logs is a filtered read, and a single request traces across services at /api/v1/logs/trace/{correlation_id} when you set your own X-Correlation-ID. See Operations & Observability.

Final Thoughts

This shape sits between a storefront and a booking application and takes something from each: like a storefront it sells several items in one basket, and like a booking application what it sells is a finite quantity attached to a moment in time. Reach for it when admission comes in tiers at different prices, each with its own quantity, when a buyer routinely buys several tickets in one transaction for people who may not be them, and when the ticket a person holds needs to exist as a record of its own — the thing that gets scanned, transferred or refunded. Where the fit against the base types is loose — the booking type inherits a helpdesk lifecycle, not an admission-ticket one — that is worth seeing rather than hiding.
If you take one thing into the build, take this: capacity is a number to check, not a limit the platform holds. Nothing decrements 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.
And what you walk away with is yours. The basket-to-order path, the state machines, and Stripe and SendGrid as keystore-held credentials stay the platform's — and a service acts on your data only through a schema that extends its base type, so importing one does nothing on its own. What you own is the schema, the buyer's access policy, and the capacity, order-to-ticket fan-out and ticket-lifecycle logic above them.