Booking Flow
This document explains how a booking moves from initial guest request to a confirmed (or rejected) meeting.
Overview
There are four parties involved:
- Guest — the person who wants to book time.
- Host — the Invito user who owns the booking page.
- Invito — the application, running as a server process.
- CalDAV server — the host's external calendar store.
Step-by-Step
1. Guest opens a booking link
The host shares one of two link types:
| Link type | URL | Effect |
|---|---|---|
| Pre-filled | /calendar/{username}/{slug} |
Guest lands directly on a specific event type |
| Generic | /calendar/{username}/ |
Guest first chooses an event type |
Both paths eventually land the guest on the slot picker for a specific event type.
2. Guest picks a date and time
The slot picker shows a date navigation. When the guest selects a date, an HTMX request fetches available slots for that day:
GET /calendar/{username}/{slug}?date=2026-05-15
Invito calculates available slots by:
- Taking the user's
availability_rulesfor that weekday. - Splitting the available window into slots of
event_type.duration_minutes. - Removing slots that overlap any
calendar_events(from CalDAV sync). - Removing slots where an existing PENDING or CONFIRMED booking exists.
The guest selects one slot.
3. Guest fills in the booking form
The form collects:
- Name (required)
- Email address (required)
- Note / message (optional)
On submit, Invito runs the double-booking check inside a transaction (see data model) and creates a Booking row with:
status = PENDINGtoken = random UUIDreserved_until = now + INVITO_BOOKING_TTL
The guest sees a confirmation page: "Your request has been sent. The host will confirm within 24 hours."
4. Invito sends the host a notification email
The email contains:
- Guest name, email, note
- Date, time, event type
- Two tokenized links:
- Confirm:
{BASE_URL}/booking/{token}/confirm - Reject:
{BASE_URL}/booking/{token}/reject
The host does not need to be logged in to act on these links.
5a. Host confirms
Host clicks the confirm link. Invito:
- Looks up the booking by
token. - Verifies
status = PENDING(idempotency: already-confirmed bookings return a "already confirmed" page). - Sets
status = CONFIRMEDin a transaction. - Writes a VEVENT to the host's primary CalDAV calendar.
- Sends a confirmation email to both host and guest with the meeting details.
5b. Host rejects
Host clicks the reject link. Invito:
- Looks up the booking by
token. - Verifies
status = PENDING. - Sets
status = REJECTED. - Sends a rejection email to the guest.
No CalDAV write occurs.
5c. TTL expires (no action)
A background goroutine runs every minute and queries:
UPDATE bookings
SET status = 'CANCELLED'
WHERE status = 'PENDING' AND reserved_until < CURRENT_TIMESTAMP
For each newly-cancelled booking, a cancellation email is sent to the guest.
State Diagram
┌─────────────┐
│ PENDING │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
Host confirms Host rejects TTL expires
│ │ │
▼ ▼ ▼
CONFIRMED REJECTED CANCELLED
All three terminal states are permanent. No transition out of a terminal state is possible.
Slot Reservation vs. CalDAV
During the PENDING window, the slot is blocked within Invito's own database but not yet written to CalDAV. This means:
- If the host also receives a direct calendar invite externally, they could confirm a booking that then conflicts with an external event (if CalDAV sync hasn't run yet).
- This is an accepted trade-off. The alternative (writing a tentative event to CalDAV immediately) would require cleanup on rejection and is more complex.
On confirmation, the CalDAV write-back is attempted. If it fails (e.g. the CalDAV server is unreachable), the booking status is still set to CONFIRMED and the write-back is retried on the next sync cycle. The booking is not rolled back.