Customer Self-Service
Customer Self-Service: Cancel + Reschedule
Booking apps (lessons, salon appointments, restaurant deposits, classes) need a way for customers to cancel or reschedule their own bookings — without phoning the owner. Web-Nothing ships this as a built-in: configure two extra fields on your booking schema and drop one component on a public page.
This works cross-device — the customer can book on their phone, then cancel from a desktop the next day. They authenticate with their booking ID + email (the same pattern Calendly, Stripe Checkout receipts, and most restaurant booking systems use).
How it works
- Customer visits your public manage-booking page.
- They enter their booking ID and the email they used to book.
- The platform looks up the booking. On a match, the page shows the booking detail with Cancel and Reschedule buttons (whichever you enabled).
- Cancel — the platform writes your configured
cancelStatusValue(typically"cancelled") into your status field. Pair this withexcludeWhen.in: ["cancelled"]on your slot's availability check so the slot frees automatically for the next customer. - Reschedule — the platform validates the new slot through the same conflict-check that gates new bookings (capacity, lead time, partition, stale-checkout window). The customer's own booking is excluded from the conflict scan, so they don't conflict with themselves.
- Both actions fire the
record_updatedworkflow trigger automatically. Wire asend_emailworkflow to that trigger and the customer gets a confirmation email — no extra code.
Setup (3 pieces)
1. Add selfServiceLookup to your booking schema
{
"name": "appointments",
"displayName": "Appointments",
"fields": [
{
"name": "slot_datetime",
"type": "datetime",
"availabilityCheck": {
"capacity": 1,
"excludeWhen": {
"field": "payment_status",
"in": ["cancelled"],
"staleAfter": { "field": "payment_expires_at", "minutesPast": 5 }
},
"minLeadTimeMinutes": 60,
"maxLeadTimeDays": 90
}
},
{ "name": "student_email", "type": "email" },
{ "name": "payment_status", "type": "string" },
{ "name": "payment_expires_at", "type": "string" }
],
"selfServiceLookup": {
"emailField": "student_email",
"allowedActions": ["cancel", "reschedule"],
"cancelStatusField": "payment_status",
"cancelStatusValue": "cancelled",
"rescheduleSlotField": "slot_datetime",
"rescheduleAlsoUpdatesField": ["payment_expires_at"]
}
}Configuration keys:
| Key | Required | What it does |
|---|---|---|
emailField | Yes | Schema field that stores the customer email. Must be type: "email" or type: "string". Compare is case-insensitive + whitespace-trimmed. |
allowedActions | Yes | Subset of ["cancel", "reschedule"]. Empty means no actions exposed (effectively turns off self-service). |
cancelStatusField | Yes | Schema field overwritten on cancel. Must be type: "string" or type: "enum". |
cancelStatusValue | Yes | Constant value written to cancelStatusField on cancel. Pair with excludeWhen.in: ["<this value>"] on your availability check so the slot frees. |
rescheduleSlotField | When "reschedule" is in allowedActions | Schema field updated on reschedule. Must be a type: "datetime" field that has availabilityCheck configured. |
rescheduleAlsoUpdatesField | Optional | Up to 5 extra schema fields that are server-stamped to the current ISO timestamp on reschedule. Typical use: ["payment_expires_at"] — see best practice below. |
idField | Optional | Defaults to "id" (auto-generated CUID). Only override if your booking schema uses a different identifier field. |
2. Mark the schema as publicly visible
In the workspace publish panel (or via the platform MCP), make sure your
booking schema is in publicVisibleSchemas. Without this, the lookup
endpoint returns 404 to all callers (anti-enumeration).
3. Drop the manage-booking component on a public page
{
"component": "manage-booking",
"props": {
"schema": "appointments",
"title": "Manage your booking",
"lookupCta": "Find my booking",
"cancelCta": "Cancel booking",
"rescheduleCta": "Reschedule",
"successMessage": "Done — confirmation email sent shortly."
}
}The component handles the entire flow: lookup form → booking detail with action buttons → confirm dialog → success state. You don't wire formData, onAction, or state machines — just drop and bind.
Best practice: pair rescheduleAlsoUpdatesField with stale-checkout protection
If your booking schema uses the stale-after pattern from the booking +
payment guide (excludeWhen.staleAfter releasing abandoned-checkout slots
after N minutes), you should include payment_expires_at in
rescheduleAlsoUpdatesField:
"rescheduleAlsoUpdatesField": ["payment_expires_at"]Why this matters. When a customer reschedules, their booking moves to
a new slot. Without the auto-stamp, payment_expires_at keeps its old
value — which might already be stale. As soon as the customer reschedules
into a fresh slot, the stale check would consider their record abandoned
and another customer could grab the slot mid-reschedule. The auto-stamp
resets the window so the customer has the full N-minute hold on the new
slot.
The platform server-stamps these fields — the customer's browser
cannot supply a custom payment_expires_at value. This defends against an
attacker rescheduling into a far-future expiry to lock up the slot.
What the customer sees
- Lookup form — two text inputs (Booking ID, Email) and a Find button.
- After the platform finds the booking — a clean card showing the booking fields, with a Reschedule button (blue) and a Cancel button (red outline). The cancel button shows a confirm dialog before firing.
- After cancel — green success card.
- After reschedule — the customer picks a new slot from a date picker
(auto-typed to
datetime-localfortype: "datetime"fields), and the platform's normal conflict check fires. If the new slot is taken, the customer sees a polite "this slot is no longer available" message and picks again.
Confirmation emails
Both cancel and reschedule fire the record_updated workflow
trigger. Set up a send_email workflow (see Workflows)
on record_updated that:
- For cancel emails: filter where
payment_status === "cancelled". - For reschedule emails: filter where
slot_datetimechanged (userecord_updated_fieldsif your workflow supports it; otherwise fire on everyrecord_updatedand let the email template be helpful for both cases).
The customer's email address is on the booking record (student_email
in the example), so the workflow already has it.
Anti-enumeration: every failure looks the same
The lookup endpoint returns identical 404 responses for ALL of:
- The app doesn't exist.
- The app is not public.
- The schema isn't in
publicVisibleSchemas. - The schema doesn't have
selfServiceLookupconfigured. - The booking ID doesn't exist.
- The booking ID exists, but the email is wrong.
This is deliberate — it prevents an attacker from probing whether a specific booking ID exists in your app. Customers who type their email incorrectly see a friendly "we couldn't find a booking matching that ID and email" message regardless of which gate failed.
Security model
- Token-based action authentication. After a successful lookup, the platform issues a short-lived signed token (default 30 minutes). The token binds the app, schema, and booking ID — it can't be replayed against a different record.
- Token never appears in the URL. It's held in browser memory and sent only in the JSON body of action requests. No browser-history leak, no referrer leak, no access-log leak.
- Rate limits. Lookup is capped at 30 requests per hour per IP per app (anti-enumeration). Cancel/reschedule actions are capped at 10 per hour per IP per app (write protection).
- Audit log. Every lookup and action attempt is logged with SHA-256-hashed booking ID and email (no plaintext PII), plus the first three octets of the caller's IP address for rough geo-binning.
- Cookie filter bypass. The customer-session cookie filter
(
customerScoped) is intentionally bypassed for self-service — the booking ID + email is the auth signal, replacing the cookie. This is what lets a customer book on their phone and cancel from a desktop the next day.
Common mistakes to avoid
- Don't forget
excludeWhen.in: [cancelStatusValue]on the slot'savailabilityCheck. Without it, cancelled bookings still block the slot. ThecancelStatusValueyou configure for self-service must appear in this list. - Don't use
customerScoped: trueAND expect cross-device cancel to work without self-service. The cookie filter is browser-bound; only self-service tokens unlock cross-device. Self-service is the correct pattern when customers might cancel from a different device than they booked from. - Don't make the email field free-text without validation. If
customers can type anything in the email field at booking time, lookup
will fail when they get the email subtly wrong (extra space, typo). Use
type: "email"so the platform validates at booking time. - Don't expose internal status fields via the cancel path. The
cancelStatusValueyou set must be one your downstream workflows / reports recognize — it's not a private internal flag.
Limitations and Phase 2
- Magic-link tokens in confirmation emails — pre-fill the lookup page via a URL token so the customer doesn't re-type booking ID + email — is a future enhancement coming in a future release. Don't roll your own email-token mechanism in the meantime; you'd duplicate what's coming.
- Auto-refund on cancel — refunds remain an owner ops decision via the Stripe Dashboard. The platform doesn't trigger refunds automatically.
- Reschedule fees — charging the customer to reschedule (e.g. "$10 if
rescheduling within 24 hours") is not built in; you can express the
rule via
minLeadTimeMinutesto reject the reschedule entirely or handle billing in your own workflow. - Customer-side audit log — a "you cancelled this booking on date X from IP Y" view for the customer is not built in.
- Email change recovery — if the customer's email changed after booking, they cannot self-cancel; they must phone the owner who cancels via the workspace.
See also
- Booking apps with availability check — the conflict-check engine that gates both new bookings and reschedules.
- Booking + payment race protection — pair self-service with the stale-checkout pattern for production-grade booking apps.
- Workflows — wire a
record_updatedworkflow to send confirmation emails on cancel and reschedule.