Source: https://www.hype-stack.dev/docs/packs-templates/packs/pack-booking

# Bookings

Appointment booking for teams that see clients: a clinic, a studio, a coach, a salon. Staff run the schedule on the web,
clients book from the web or the mobile app, and people with no account can book as guests and confirm by email. This is
the `pack-booking` pack. The [Clinic](/docs/packs-templates/templates/clinic) and
[Personal Trainer](/docs/packs-templates/templates/personal-trainer) templates are built on it.

## What you need

- Postgres, plus object storage for the business logo and photos on client notes (bucket `booking-media`, created on
  first upload). Both are base services.
- Optionally a **Resend** key (`RESEND_API_KEY` from the base template). Without it no email goes out, which also means
  guests never get their confirmation link. Outside production the link is printed to the backend log instead.
- No payment provider. Payments, deposits, waitlists, SMS, and multiple locations are out of scope.

## What it depends on

- A **SaaS starter** for auth, organizations, and permissions. The business is an organization and its staff are
  members.

## Who uses it

| Who            | What they are                                                                           | Where they work                            |
| -------------- | --------------------------------------------------------------------------------------- | ------------------------------------------ |
| **Team**       | Members of the organization                                                             | The web app, under the Control center      |
| **Specialist** | A team member people can book. A front desk can run the schedule without being bookable | Same                                       |
| **Client**     | Someone who books. Signed in, but not a member of the organization                      | `/book` and `/my-bookings`, web and mobile |
| **Guest**      | Someone who books with no account                                                       | The public booking page and email links    |

## What you get

**For the team** (web only, each page needs `manage:booking`)

- `/booking`: the schedule. Every specialist's week, drag to move, click an empty slot to book.
- `/booking/requests`: approve or decline new bookings and requests to move, with undo.
- `/booking/clients`: the client list, late cancels and no-shows, notes with photos or video, block and unblock.
- `/booking/events`: workshops and group classes with several hosts and a capacity.
- `/booking/availability`: weekly hours and time off, with a preview of which bookings a change would hit.
- `/booking/setup`: services, specialist profiles, and business settings.

**For clients**

- `/book`: specialists with their open times, saved favourites, and events.
- `/my-bookings`: upcoming, past, and cancelled visits. Move, cancel, or book again.
- On mobile, the same two screens plus a client app at `/client` for people who belong to no organization. The mobile
  app has no staff screens.

**For guests**

- A public directory API and a `GuestBookingForm` component. The pack does not ship a public landing page; the templates
  put an open calendar on theirs, and you can do the same.
- `/booking/visit?token=`: the guest's own visit page, linked from the email.

## How guest booking works

1. The guest picks a time and enters a name and email. The booking is saved as `unconfirmed` and holds the time for 30
   minutes.
2. One email goes out, to the guest only, with a link.
3. Opening the link and confirming turns it into a normal booking: approval rules apply and the usual emails go out.
4. An unconfirmed hold frees its time after 30 minutes on its own. A job deletes the row later.
5. One email address can hold at most 3 unconfirmed bookings per business.

Guests cannot cancel or move from the visit page. When someone later signs up with the same verified email, every
booking under that email moves to their account. An unverified email links nothing.

Online booking is on by default. Turn it off in Setup > Business, and only the team can book new people; existing
clients keep booking.

## Who can see what

| Permission                | Who has it                | What it allows                                                                                                                 |
| ------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `manage:booking`          | members and admins        | Run the schedule, requests, clients, events, and their own hours and time off                                                  |
| `manage:booking_settings` | admins (via `manage:all`) | Services, business settings, logo, and location. Also see every specialist's private notes and edit another specialist's hours |

Without `booking_settings` a member manages only their own hours, profile, and notes.

Clients never pass an organization permission check. `/booking-client/*` finds the business from the client's own
record, not from the session's active organization, so a client sees only their own bookings at the businesses they
booked with. A blocked client can no longer book from the app; the team still can book for them.

## Use it from your code

Every write goes through two functions. They take the row lock, check the transition is allowed, record history for
undo, and queue the emails. Do not write to the `booking` table yourself.

```ts
import { createBooking } from "@backend/features/booking/modules/create-booking";
import { changeBooking } from "@backend/features/booking/modules/change-booking";

const { booking } = await createBooking({
  organizationId,
  actor: { kind: "team", id: user.id },
  sessionTypeId,
  specialistId: "any",
  clientId,
  startAt: new Date("2026-11-04T10:00:00Z"),
});

await changeBooking({
  organizationId,
  bookingId: booking.id,
  action: "cancel",
  actor: { kind: "system", id: "" },
  reason: "Clinic closed",
});
```

`actor.kind` is `team`, `client`, or `system`, and it decides which transitions are allowed and who gets emailed.
`specialistId: "any"` lets the pack pick whoever is free.

Other entry points in `features/booking/modules/`:

| Module             | Use it for                                                                 |
| ------------------ | -------------------------------------------------------------------------- |
| `availability`     | `findAvailableSlots`, `checkSlot`: open times for a service and specialist |
| `public-directory` | `getPublicDirectory`: what a signed-out visitor can book                   |
| `guest-booking`    | `bookAsGuest`, `getGuestVisit`, `confirmGuestVisit`                        |
| `settings`         | `getOrgSettings`, `updateOrgSettings`                                      |
| `clients`          | `listClients`, `createClient`, `blockClient`                               |

On the frontend, embed `GuestBookingForm` with a pick of `{ sessionTypeId, specialistId, startAt }` to let signed-out
visitors book from any public page. Fetch open times with `sdk.bookingPublic.directory`.

## Background work

Three jobs on the template's scheduler and one queue:

| Job                            | Schedule         | What it does                                                                              |
| ------------------------------ | ---------------- | ----------------------------------------------------------------------------------------- |
| `booking-reminders`            | every 5 minutes  | Queues reminders and "confirm you are coming" requests that are due                       |
| `booking-daily-agenda`         | every 5 minutes  | Sends each specialist their day, at 08:00 local or earlier before an early first visit    |
| `booking-reschedule-deadlines` | every 15 minutes | Cancels bookings whose "pick a new time" deadline passed, and deletes expired guest holds |

Every email goes through the `booking-notify` queue. A change is queued in the same transaction, 10 seconds out, so an
undo inside that window sends nothing and several quick edits arrive as one email. Each email carries an `.ics` file
that updates the same calendar entry instead of adding a new one. A ledger table makes sure no message is sent twice.

## Emails

Clients get: confirmed, requested, approved, declined, moved, cancelled, move requested, move declined, pick a new time,
reminder, and the guest confirmation. Specialists get: new booking, new request, client cancelled, client moved, move
request, attendance confirmed or declined, and the daily agenda.

Every client email except the reminder is mandatory. The rest follow the business defaults set in Setup, then each
person's own preferences. All emails use one React Email component in `modules/notify/emails/booking-email.tsx`, and the
wording lives in the `booking_email_*` keys of the backend message catalog.

In-app notifications are off. The pack has an in-app channel in `modules/notify/channels/in-app.ts`, but it is a no-op,
and installing the [Notifications](/docs/packs-templates/packs/pack-notifications) pack does not switch it on. To get
booking updates in the inbox, set `IN_APP_AVAILABLE = true` there and call `sendNotification` from `sendInAppMessage`.

## Customize

| What                                                                     | Where                                                                                      |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| Services, lengths, buffers, seats, prices, intake questions              | Setup > Services in the app                                                                |
| Weekly hours and time off                                                | `/booking/availability`                                                                    |
| How far ahead people can book                                            | Setup > Team, per specialist (1 to 365 days)                                               |
| Time zone, currency, reminder lead time, approval policy, online booking | Setup > Business                                                                           |
| Approval rules in code                                                   | `helpers/approval-policy.ts`: client override, then business policy, then the service rule |
| Guest hold length and per-email cap                                      | `helpers/holds.ts`                                                                         |
| Email layout and copy                                                    | `modules/notify/emails/booking-email.tsx`, `booking_email_*` messages                      |

## Data model

| Model                                        | Table                                            | What it holds                                             |
| -------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
| `BookingSettings`                            | `booking_settings`                               | One row per business: name, time zone, policies, defaults |
| `BookingLocation`                            | `booking_location`                               | Address and payment note                                  |
| `BookingSpecialist`                          | `booking_specialist`                             | A bookable team member                                    |
| `BookingSessionType`                         | `booking_session_type`                           | A service                                                 |
| `BookingSpecialistHours`                     | `booking_specialist_hours`                       | Weekly hours, several ranges per day                      |
| `BookingTimeOff`                             | `booking_time_off`                               | Time away                                                 |
| `BookingClient`                              | `booking_client`                                 | Someone who books, with an optional user account          |
| `BookingFavorite`                            | `booking_favorite`                               | A specialist a client saved                               |
| `Booking`                                    | `booking`                                        | A booking or a recurring series                           |
| `BookingEvent`                               | `booking_event`                                  | A workshop; each seat is a `Booking`                      |
| `BookingOccurrenceState`                     | `booking_occurrence_state`                       | Attendance per visit of a series                          |
| `BookingActivity`                            | `booking_activity`                               | Every change with before and after, used for undo         |
| `BookingNotification`                        | `booking_notification`                           | Every message sent, so nothing is sent twice              |
| `BookingClientNote`, `BookingNoteAttachment` | `booking_client_note`, `booking_note_attachment` | Team notes about a client, with files                     |

## API

| Prefix                | Who can call it                                                         |
| --------------------- | ----------------------------------------------------------------------- |
| `/booking`            | Staff: `manage:booking`, settings routes need `manage:booking_settings` |
| `/booking-client`     | Any signed-in user, scoped to their own client records                  |
| `/booking-public`     | Anyone: the directory, guest booking, and the guest's visit page        |
| `/booking-attendance` | Anyone with a token from an email                                       |

The public routes have no rate limit beyond the guest cap and a 30-second cache on the directory. Put a per-IP limit in
front of `/booking-public` before you point real traffic at it.

## Environment variables

None of its own. `RESEND_API_KEY` from the base template turns email on.

## After installing

Run `pnpm prisma generate` and then `pnpm prisma migrate dev` in your backend app, sign in, and open `/booking/setup` to
add your services and specialists.

## Related

- [Clinic template](/docs/packs-templates/templates/clinic)
- [Personal Trainer template](/docs/packs-templates/templates/personal-trainer)
- [Access control](/docs/backend/access-control)
