Appointment & Service Booking
A priced service catalog, a set of staff who deliver it, and appointments that move through a real state machine — requested, confirmed, completed, cancelled, no_show. What the appointment service handles, and why contention is still your problem.
The Project
- •What you sell has a duration as well as a price, and the duration determines how much of the day it consumes.
- •Delivery is tied to a named person or resource, and customers care which one.
- •An appointment has a lifecycle that outlives the booking moment — confirmed, rescheduled, completed, cancelled, and the awkward fifth case of the customer simply not turning up.
- •Reminders matter, because unreminded appointments become no-shows.
- •Staff need a calendar view and a customer history, and customers need a self-service booking surface.
ℹ️ The exemplar behind this page
Architecture & Design
{
"name": "booking",
"namespace": "luxe_locks",
"schema_type": "object",
"schema_content": {
"name": "booking",
"parent_type": "tenant",
"extends": "appointment:base_appointment",
"attributes": [
{"name": "start_time", "type": "string", "mandatory": true},
{"name": "end_time", "type": "string", "mandatory": true},
{"name": "stylist_uuid", "type": "string"},
{"name": "booked_at", "type": "string"},
{"name": "hair_length", "type": "string"},
{"name": "hair_density", "type": "string"},
{"name": "first_time_visit", "type": "boolean"},
{"name": "cancellation_reason","type": "string"},
{"name": "check_in_time", "type": "string"}
],
"references": [
{"name": "Stylist", "cardinality": "one", "role": "resource"},
{"name": "Customer", "cardinality": "one", "role": "actor"},
{"name": "Service", "cardinality": "one", "role": "service"},
{"name": "ServiceAddon", "cardinality": "many"}
]
}
}| Type in luxe_locks | Extends | Notes |
|---|---|---|
| booking | appointment:base_appointment | inherits status of requested, confirmed, cancelled, completed or no_show, plus duration_minutes, confirmation_code, reminder_sent_at, and confirmed_at / cancelled_at / completed_at / no_show_at |
| service | service:base_service | inherits price, currency, duration_minutes, pricing_unit and a status of active, inactive or discontinued; adds category, requires_consultation, aftercare_instructions and maintenance_weeks |
| service_addon | service:base_service | the same base type used a second time for optional extras; references Service with cardinality many |
| customer | customer:base_customer | adds salon-specific history — hair_type, hair_history, allergies, referral_source |
| attachment | attachment:base_attachment | file records, backed by your own S3 bucket |
| stylist | none | a plain tenant-parented record: full_name, email, photo, specialization, is_active, years_experience, working_days |
| time_slot | none | a plain record with date, start_time, end_time, is_booked, slot_type and block_reason, referencing Stylist as resource |
💡 Why stylist and time_slot extend nothing
The End-to-End Flow
- 1
Choose a service
The customer browses service records. duration_minutes and price come from base_service, so the surface knows both how long the booking will run and what it costs without any extra modelling. Add-ons are service_addon records with a many-reference back to the services they pair with.
- 2
Check availability
appointment / check_provider_availability is a read-only query over existing appointments for a resource in a window, using half-open interval comparison so that a booking ending at two o'clock does not collide with one starting at two. It answers a question. It does not reserve anything.
- 3
Book
appointment / schedule_appointment moves the record from requested to confirmed and stamps confirmed_at. Calling it twice is idempotent — the original timestamp is preserved rather than restamped — so a double-submitted form does not produce two bookings from one request.
- 4
Confirm to the customer
base_appointment carries confirmation_code. Delivering it is the email or SMS service: a send_email or send_sms operation, or a workflow bound to the create event on the booking type so it happens without an explicit call.
- 5
Remind
reminder_sent_at is a field on the appointment. Something has to set it — a scheduled workflow that queries tomorrow's confirmed bookings and calls send_sms is the usual shape. The platform does not run a reminder loop on your behalf.
- 6
Reschedule
appointment / reschedule_appointment changes start_time and end_time and leaves the status alone. This one is deliberately field-only and is not idempotent: it is an edit, not a transition. A rescheduled appointment stays confirmed.
- 7
Close it out
complete_appointment, cancel_appointment or mark_no_show, each stamping its own timestamp. The state machine refuses the illegal paths — completing an appointment that was already cancelled returns HTTP 409 with INVALID_STATE_TRANSITION, and the record keeps its previous state and its references intact.
ℹ️ Two audiences, one dataset
What You Get Without Building It
- •The appointment state machine, with idempotent transitions, per-transition timestamps, and 409 on an illegal move.
- •A read-only availability query with correct half-open overlap semantics, so you are not writing interval arithmetic.
- •A priced, timed service catalog from one base type — used twice in this app, once for services and once for add-ons.
- •Customer profiles tied to user accounts, so a returning customer's history is a reference walk rather than an email-address match.
- •SendGrid email and Twilio SMS as configured integrations: four keys for email, three for SMS, held in the keystore and masked on read.
- •File storage on your own S3 bucket for stylist photos and service imagery.
- •A staff admin panel and a public booking surface generated from the schemas, plus a calendar configuration for the appointment view.
- •Tenancy and the platform roles underneath, so a multi-location chain is a tenancy decision rather than a schema change.
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 the keys for email and SMS in the keystore (SendGrid, Twilio) so confirmations and reminders can send — see Email & Notifications. This shape takes no payment by default, so there is nothing to credential on the money side unless you add it.
What you write yourself
⚠️ Double-booking is not prevented for you
| Decision | The state of it in the exemplar |
|---|---|
| is_booked on a time_slot | A boolean with a default of false that nothing flips. Generating the slot grid, marking slots taken, and reconciling that with the appointments themselves are all yours. |
| Working hours and blackout dates | time_slot carries slot_type and block_reason, and stylist carries working_days as a string. Neither is interpreted by anything. Rota rules are application logic. |
| Taking money | This exemplar imports no payment service and defines no payment type — it books, it does not charge. Deposits, no-show fees and pay-at-booking mean adding a type that extends payment:base_payment and importing that service. The platform's own scheduling test composes appointment with payment for exactly this, so the composition is available; it is simply not in this app. |
| Reminders | reminder_sent_at is a field. The schedule that populates it is a workflow you write with trigger_source scheduled. |
| Cancellation policy | cancel_appointment is an operation, not a rule. Notice periods, fees and who may cancel whose booking are yours — the last of those is an access-policy question. |
| Denormalised display fields | The generated booking type carries stylist_name and service_name alongside the real references. They are a display convenience and nothing keeps them in step with the referenced records. Either maintain them in a workflow or read through the reference. |
| Recurring appointments | A standing weekly slot is a series of records you generate. There is no recurrence rule on base_appointment. |
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
Where to Go Next
The mechanism
Transactional Services
The appointment state machine, services/execute, idempotency and the 409. Read it
The gaps
Workflows & Automation
Where reminders, contention rules and cancellation policy actually live. Read it
The messages
Email & Notifications
SendGrid and Twilio as keystore credentials, with templates and campaigns as records. Read it
The surfaces
Dynamic UI
How the booking calendar, admin sections and metric cards are declared rather than coded. Read it
On this page