Hype StackHypeStack

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 and 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

WhoWhat they areWhere they work
TeamMembers of the organizationThe web app, under the Control center
SpecialistA team member people can book. A front desk can run the schedule without being bookableSame
ClientSomeone who books. Signed in, but not a member of the organization/book and /my-bookings, web and mobile
GuestSomeone who books with no accountThe 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

PermissionWho has itWhat it allows
manage:bookingmembers and adminsRun the schedule, requests, clients, events, and their own hours and time off
manage:booking_settingsadmins (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/:

ModuleUse it for
availabilityfindAvailableSlots, checkSlot: open times for a service and specialist
public-directorygetPublicDirectory: what a signed-out visitor can book
guest-bookingbookAsGuest, getGuestVisit, confirmGuestVisit
settingsgetOrgSettings, updateOrgSettings
clientslistClients, 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:

JobScheduleWhat it does
booking-remindersevery 5 minutesQueues reminders and "confirm you are coming" requests that are due
booking-daily-agendaevery 5 minutesSends each specialist their day, at 08:00 local or earlier before an early first visit
booking-reschedule-deadlinesevery 15 minutesCancels 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 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

WhatWhere
Services, lengths, buffers, seats, prices, intake questionsSetup > Services in the app
Weekly hours and time off/booking/availability
How far ahead people can bookSetup > Team, per specialist (1 to 365 days)
Time zone, currency, reminder lead time, approval policy, online bookingSetup > Business
Approval rules in codehelpers/approval-policy.ts: client override, then business policy, then the service rule
Guest hold length and per-email caphelpers/holds.ts
Email layout and copymodules/notify/emails/booking-email.tsx, booking_email_* messages

Data model

ModelTableWhat it holds
BookingSettingsbooking_settingsOne row per business: name, time zone, policies, defaults
BookingLocationbooking_locationAddress and payment note
BookingSpecialistbooking_specialistA bookable team member
BookingSessionTypebooking_session_typeA service
BookingSpecialistHoursbooking_specialist_hoursWeekly hours, several ranges per day
BookingTimeOffbooking_time_offTime away
BookingClientbooking_clientSomeone who books, with an optional user account
BookingFavoritebooking_favoriteA specialist a client saved
BookingbookingA booking or a recurring series
BookingEventbooking_eventA workshop; each seat is a Booking
BookingOccurrenceStatebooking_occurrence_stateAttendance per visit of a series
BookingActivitybooking_activityEvery change with before and after, used for undo
BookingNotificationbooking_notificationEvery message sent, so nothing is sent twice
BookingClientNote, BookingNoteAttachmentbooking_client_note, booking_note_attachmentTeam notes about a client, with files

API

PrefixWho can call it
/bookingStaff: manage:booking, settings routes need manage:booking_settings
/booking-clientAny signed-in user, scoped to their own client records
/booking-publicAnyone: the directory, guest booking, and the guest's visit page
/booking-attendanceAnyone 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.