Scheduling model
- Booking page (
Admin → Booking page): each workspace member has a public profile at/<username>with a display name, bio, timezone and photo (uploaded, resized to 512×512, or linked by URL). Every public page carries the workspace name with the Bookly mark and a "Powered by Bookly" footer; on plans that remove branding (Pro, Team, self-hosted) the logo URL and accent colour fromAdmin → Settings → Brandingreplace them, and the colour drives buttons, selected days and focus rings. - Availability (
Admin → Availability): a schedule holds weekly hours in its own timezone plus date overrides (custom hours or a whole day off). A default Mon–Fri 9–17 schedule is created with the profile. Members can keep several schedules (say, "Working hours" and "Evenings"); a new one starts as a copy of the default, any of them can be made the default, and each event type picks the schedule it uses, falling back to the default when its schedule is deleted. Date overrides belong to a schedule; a day off can be applied to all of a member's schedules in one go (the default when adding one), shows as "all schedules", and is removed everywhere with one click. - Event types (
Admin → Event types): what people can book — duration, slot interval, buffers before/after, minimum notice, booking horizon, max bookings per day, one or more locations (Bookly video, Google Meet, Zoom, Teams, phone, in person, custom; with several, the attendee picks one when booking and the first is the default; a phone call asks the attendee for their number with the country preselected from their location, and an in-person meeting whose address is left blank asks the attendee where to meet; both appear in the invitation, emails and booking pages), booking questions, "requires confirmation", hidden (link-only), and guests: how many colleagues the attendee may bring (0 = none). Guests are listed on the booking, added to the calendar event, and get the invitation, reminders and any cancellation; only the person who booked can change or cancel. - Bookings (
Admin → Bookings): upcoming and past, confirm pending requests, cancel with a reason. Attendees get a manage link (/booking/<token>) to cancel, reschedule or download the.ics. With the bookings calendar (rolling out behind thebookings_calendarflag) the page has two views, remembered per browser: the list, paged 50 at a time with "Show more", and a calendar by day, week or month in your timezone (← → step, T today, D W M switch). Every booking is tinted with its event type's colour, a legend counts the event types in view, and group sessions show as one block with their seats. Clicking a booking opens it in a side pane over the page (?booking=<id>, so it can be linked); J and K move to the next and previous booking in the view, Escape closes it, and "Open page" goes to the full booking. Owners and admins see everyone's bookings with a host filter; members see their own.
Priority-aware availability#
Your calendar is governed by intent, not just busy/free:
- Focus blocks: mark a range in
Admin → Availabilityas focus. It counts as available only for priority contacts; everyone else sees your open hours. - Weekly budget: a cap on Bookly bookings per week for new people (calendar events only block the times they cover; they are not counted, and neither are they for "max bookings per day"). Once the week is full, only priority contacts can still book.
- Priority contacts are existing relationships: contacts in stage active or won, or tagged
vip. Bookly recognises them by email, at booking time (the form email is checked before the slot is validated) and on the slot list when the visitor is known, for example when they arrive from a routing form or a link with?email=; the API acceptsemailon/availabilityfor the same purpose.
Group sessions (seats)#
Seats per slot on an event type (default 1) turns it into a group session: the same start time can be booked by several people until it is full. Every time on the public page shows "N seats left" (the API reports the same in seatsLeft), the attendee's booking page shows the session's numbers ("2 / 3 seats", how many seats are still open, how many people are waiting), and Admin → Bookings folds the bookings of one session into a single card with the seat count, every attendee (name, company, email, status, own actions) and the people waiting for that session; the booking's brief page lists the same attendees. Everyone in a session gets the same meeting link, and the host's calendar holds one event for the session with every attendee as a guest; the guest list is updated as people book or cancel, and the event moves to the next attendee if the first one cancels. Sessions at other times block the host like any booking.
Recurring bookings#
Turn on Recurring bookings on an event type and choose the frequency (daily, weekly, monthly), the interval and the number of sessions (2–52). An attendee picks the first time and sees the whole series before confirming; dates the host cannot take are skipped and reported. Every occurrence is its own booking (own meeting link, calendar event, reminders and manage link) tied together by a series id, so a single session can be rescheduled or cancelled, and the manage page also offers "cancel all remaining sessions". The confirmation email lists all dates and attaches one .ics with every occurrence. A paid series is charged in a single Stripe Checkout (price × sessions); cancelling one occurrence refunds that occurrence's share.
Waitlist#
Turn on Waitlist on an event type to let people ask to be told when a spot opens (off by default). With it on, a full group session still shows on the booking page, in its place in the day, as "Full · waitlist" (or "Full · 2 waiting" once people are queued), and a day with no free times offers a waitlist for that day; with it off, a full session shows as "Full" and cannot be joined. People leave a name and email and get a confirmation with a leave link. On a cancellation Bookly emails the first person waiting for that session (one per freed seat) and everyone waiting for that day, with a link straight to the freed time; the spot goes to whoever books first. Hosts see and can remove entries under Admin → Bookings.
Abuse controls#
- Public forms (booking, waitlist, routing) accept at most 10 submissions per visitor (IP) per 10 minutes, and every form has a honeypot field.
Admin → Settings → General → Block bookings fromrefuses named emails or whole@domains.- In cloud mode the Free plan allows 100 new bookings per month per workspace; API keys get a per-plan request budget (60 / 600 / 1200 per minute). Self-hosted installs have no quotas.
Buffers#
Buffer before is the free time an event type needs before each of its bookings, buffer after the free time after. They are applied to the candidate slot: a slot is offered only if [start − before, end + after] does not overlap any existing booking or external busy block.
How slots are computed#
src/server/availability/engine.ts is pure and unit-tested: weekly rules and overrides (schedule timezone) → minus existing bookings padded with the event's buffers → minus external busy time (calendar sync, K2) → minus minimum notice and horizon → grouped by the visitor's calendar day. The public page detects the visitor's timezone and lets them change it.
Emails#
Confirmation to the attendee (with .ics, METHOD:REQUEST), notification to the host, cancellation to both (METHOD:CANCEL), and reminders at each event type's offsets (default 24h and 1h before), scheduled precisely per booking through the job queue (pg-boss in-process, or QStash on serverless) with a booking.reminders sweep every 5 minutes as the safety net; without either, call /api/cron/tick with Authorization: Bearer $CRON_SECRET (the repo ships a GitHub Actions schedule in .github/workflows/reminders.yml, enabled with the REMINDERS_ENABLED repository variable). All go through @bookly/email (console / Resend / SMTP).