Booking & Scheduling
Prov0.1.0Public booking pages for your services: hosts publish weekly hours and date overrides, visitors pick a free slot in their own time zone, and bookings can be confirmed, rescheduled, cancelled and added to any calendar.
terminalnpx ajolla add bookingWhat you get
- Public /book/[slug] pages with a slot picker in the visitor's time zone
- DST-correct slot engine with buffers, minimum notice and booking windows
- Weekly hours plus per-date overrides for every host
- Database-enforced no double booking (exclusion constraint)
- Guest manage links to reschedule or cancel, plus .ics calendar files
- Optional manual confirmation, email notifications and reminders
- Admin screens for hosts, services, availability and bookings
Routes it adds
/admin/booking/admin/booking/hosts/admin/booking/hosts/[hostId]/admin/booking/services/admin/booking/services/[serviceId]/admin/booking/services/new/book/[slug]/book/manage/[token]/bookings
/api/ajolla/booking/[bookingId]/ics/api/ajolla/booking/reminders
Plus 1 Supabase migration with row-level security, and an AGENTS.md your coding agent reads after install.
Installs with
Kernel (added automatically).
Configuration
- NEXT_PUBLIC_SITE_URL (required)
- Absolute URL of your app (e.g. https://example.com); used for manage links in notifications.
- BOOKING_HOST_EMAIL
- Fallback address for host notifications when a host has no email of its own.
- BOOKING_REMINDER_HOURS
- Send reminders for bookings starting within this many hours (default 24).
Events
Other modules (and your own code) can react to these:
booking.createdbooking.confirmedbooking.cancelledbooking.reschedulednotifications.requested
Use it from any stack
Ajolla Hosted runs the same code as a multi-tenant API. Create an app in your dashboard, enable Booking & Scheduling, and call /api/m/booking/v1 with your app key.
OpenAPI document — feed it to Ajolla to generate SDKs in 7 languages and an MCP server.
AGENTS.md
Read the instructions installed with this module
markdown# Ajolla Booking & Scheduling
Public booking pages for your services. Hosts publish weekly hours (in their own IANA time zone) plus per-date overrides; visitors pick a free slot in any time zone, book with name/email, and get a manage link to reschedule, cancel or add the booking to their calendar.
## What was installed
**Routes**
- `/book/[slug]` — public booking page: service details, time-zone picker, the next 14 days with availability, slot list, booking form.
- `/book/manage/[token]` — the visitor's booking (also the success page): details, "Add to calendar", reschedule, cancel. Not indexed; `Referrer-Policy: no-referrer`.
- `/bookings` — the signed-in user's bookings (made while signed in, or as a guest with their *confirmed* email address).
- `/admin/booking` — bookings with filters; confirm pending ones, cancel any (permission `booking.manage`).
- `/admin/booking/hosts`, `/admin/booking/hosts/[hostId]` — hosts, weekly hours, date overrides.
- `/admin/booking/services`, `/admin/booking/services/new`, `/admin/booking/services/[serviceId]` — services.
- `GET /api/ajolla/booking/[bookingId]/ics?token=…` — iCalendar file; requires the booking's manage token.
- `GET /api/ajolla/booking/reminders` — reminder cron (Bearer `CRON_SECRET`).
**Code** — `lib/ajolla/booking/core.ts` (all business logic, framework-free), `slots.ts` (pure slot engine), `time.ts` (DST-safe `Intl` time-zone math), `ics.ts` (RFC 5545), `view.ts` (formatting/form helpers), `app/book/_lib/` (server helpers), `components/ajolla/booking/*`, `lib/ajolla/admin/booking.ts`.
**Tables** (`ajolla_booking_`): `hosts`, `services` (unique `slug`), `rules` (weekly hours), `overrides` (per date: no times = closed; times replace weekly hours), `bookings`. `bookings.blocked` is set by a trigger to `[starts_at − buffer_before, ends_at + buffer_after)`, and an exclusion constraint (btree_gist) forbids overlapping non-cancelled bookings per host — double booking is impossible even under races; the core reports it as `slot_unavailable` (409).
**RLS** — anonymous visitors: no direct access. Customers: read their own bookings (by `user_id`, or by confirmed email) and the services/hosts of those bookings. `booking.manage`: everything. Public pages and manage links use the secret-key client on the server, **only** through core functions that validate first (slot against live availability, token against its sha256 hash). Only token hashes are stored; the plaintext is shown once (redirect to the manage page) and emailed.
**Events** — `booking.created`, `booking.confirmed`, `booking.cancelled`, `booking.rescheduled` (payload: ids, status, times — no customer data or tokens), and `notifications.requested` for the customer and the host on each change, plus reminders. Install Notifications to deliver them as email; without it, nothing breaks.
**Env** — `NEXT_PUBLIC_SITE_URL` (absolute links in notifications), `BOOKING_HOST_EMAIL` (fallback host address), `BOOKING_REMINDER_HOURS` (default 24), and the kernel's `CRON_SECRET`.
## Setup
1. Apply migrations: `supabase db push`.
2. Set `NEXT_PUBLIC_SITE_URL` (and optionally `BOOKING_HOST_EMAIL`).
3. As an admin, open `/admin/booking/hosts`, add a host, set weekly hours; then add a service at `/admin/booking/services`. Share `/book/<slug>`.
4. Schedule reminders, e.g. in `vercel.json`: `{ "crons": [{ "path": "/api/ajolla/booking/reminders", "schedule": "0 * * * *" }] }`.
5. Install Notifications (`npx ajolla add notifications`) to send the emails.
## Customising
- Slot rules live in `lib/ajolla/booking/slots.ts` (`computeSlots`): slots step by the service's interval from the start of each open range, must end inside it, respect minimum notice and the booking window, and their buffered span may not touch another booking's buffered span. Skipped DST wall times move forward; repeated ones use the earlier instant.
- Notification wording: `bookingNotifications()` in `core.ts`. Confirm/cancel/reminder messages sent later carry no manage link, because only the token's hash is stored.
- Days shown per page: `PICKER_DAYS` in `app/book/_lib/picker.ts`. Styles: `components/ajolla/booking/booking.module.css` and the kit's `--aj-*` variables.
- The public booking action is unauthenticated by design; put a rate limit (e.g. Vercel Firewall) or a CAPTCHA in front of `/book/*` POSTs if you see abuse.