Build Acuity Scheduling
KINDAreplaces $20/mosaves $240/yrback to the verdict
A client booking system where the correctness problem is a race between your calendar and your booking page: availability computed a second ago may already be gone, and the person who loses that race is a client standing in front of a closed door.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the server, the sync workers and the notification queue.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree locally
Why Slot reservation correctness depends on real transactions and row contention, which is exactly what this is for.
Get it Docker or your package manager. open ↗
Verify
Two concurrent transactions reserving one slot serialise correctly - accountfree
Why Recurrence with exceptions, declined invitations and all-day events are the cases that break availability, and none appear in an empty test calendar.
Get it A calendar with a month of realistic meetings including a recurring series with a cancelled instance.
Verify
Availability correctly excludes the series but includes the cancelled instance - accountfree to test
Why Deposits, webhook confirmation and refunds behave differently from a single charge, and the redirect failure only appears against a real flow.
Get it Sign up, use test keys, configure a webhook, and test paying then closing the browser immediately. open ↗
Verify
A payment with the browser killed still produces a booking - decidefree
Why A policy written in prose gets implemented twice slightly differently and the client sees whichever path ran.
Get it Express it as bands of hours before the appointment and refund percentages, and decide what happens when you cancel.
Verify
The policy is a table of bands with an explicit rule for your own cancellations
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- services(id, name, duration_minutes, buffer_before, buffer_after, capacity, price_minor, cancellation_policy_id) - availability_rules(id, service_id, day_of_week, start_local, end_local, timezone, valid_from, valid_to) - calendars(id, provider, account_ref, sync_token, last_synced_at, direction) · direction is read, write or both - busy(id, calendar_id, starts_at, ends_at, external_event_id, is_ours) - bookings(id, service_id, starts_at, ends_at, client_id, state, created_at, external_event_id, payment_id) - holds(id, service_id, starts_at, expires_at, session_id) · counted against capacity while alive - A booking and its calendar event are linked by a stored identifier, so the tool can recognise its own events and never touch anyone else's.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://localhost:5432/bookings | Client bookings and intake responses, which may include health information. |
CALENDAR_CLIENT_IDsecret | required | a provider client id | Create and update only the tool's own events. |
HOLD_MINUTES | required | 10 | Long enough to pay, short enough not to block a popular slot. |
SYNC_STALE_MINUTES | required | 30 | Beyond this availability narrows rather than being offered optimistically. |
STRIPE_WEBHOOK_SECRETsecret | required | whsec_... | Bookings confirm from webhooks, so an unverified endpoint books appointments. |
TOUCH_FOREIGN_EVENTS | required | never | Present as a constant. There is no other value. |
The build, in order
Availability three sources agree on
An offered slot is genuinely free.
Calendar, rules and bookings. Any one alone offers a slot that is taken.
Files
src/availability.tsThe series minus its cancellations, not the series.
Files
src/recurrence.tssrc/recurrence.test.tsNarrow availability rather than offering optimistically.
Files
src/availability.ts
done when · tick each as it passeswatch out- Computing availability from your rules alone and checking the calendar later means a slot is offered during a meeting, and the client books it. Combine all three sources before offering anything, and treat a stale calendar sync as a reason to narrow availability rather than to widen it.
Booking a slot exactly once
Two people booking the last slot simultaneously produce one booking.
Check-then-insert double-books when the page is busy.
Files
src/booking.tsTwenty parallel requests. Sequential tests prove nothing here.
Files
tests/concurrency.test.tsThe client is already committed to booking something.
Files
src/booking.ts
done when · tick each as it passeswatch out- Checking availability and then inserting a booking is a race with a window of a few hundred milliseconds, and it double-books precisely when a page is popular. Take the slot within the booking transaction, and let the database serialise the contention.
Writing to your calendar, and nothing else
A booking appears in your calendar and no real meeting is ever touched.
A rename must not orphan a booking or endanger a meeting.
Files
src/calendar.tsIn the write path, tested by attempting it.
Files
tests/foreign-events.test.tsToken expiry is routine and a duplicated week is very visible.
Files
src/sync.ts
done when · tick each as it passeswatch out- Identifying your own events by title means a renamed booking becomes invisible to the tool, which then creates a duplicate, or a coincidence of naming lets it modify a real meeting. Store the created event identifier and treat everything else as untouchable.
Holds, deposits and payment
A client filling in details does not lose the slot, and paying is not lost either.
Abandoned checkouts never come back to release the slot.
Files
src/holds.tsTest by killing the browser after paying.
Files
src/webhooks.tsIt happens, and double-booking is not the answer.
Files
src/holds.ts
done when · tick each as it passeswatch out- Confirming a booking on the browser redirect loses the booking whenever a client pays and closes the tab, which happens constantly on a phone, and you have their money and no appointment. The webhook is the source of truth and the redirect is a convenience.
Reminders, cancellation and the client's experienceproduct builder
Both parties know what is happening and cancelling works as promised.
Labelled. It is the message that must not be misread.
Files
src/notify.tsThe quoted figure is the refunded figure, tested.
Files
src/refund.tssrc/refund.test.tsSo the information is there when the appointment starts.
Files
src/intake.ts
done when · tick each as it passeswatch out- Sending a reminder with the time in your timezone rather than the client's is how someone arrives an hour late having read it correctly, and it happens on every cross-border booking. Send times in the recipient's zone, labelled, and include yours as a secondary.
That is the whole plan for Acuity Scheduling. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Multi-person availability scheduling.
- Staff rostering and resource allocation.
- Two-way sync for foreign events.
- Cross-jurisdiction tax and invoicing.
- race-free booking under load
- team routing and round robin
- reliable reminders and timezone edge cases
- calendar-provider edge cases and timezone correctness
- multi-calendar conflict handling
- Waitlists for full slots, which turn a cancellation into a booking
- Recurring appointments, which multiply every timezone and exception question here
- Group classes with per-attendee intake, once single bookings are provably correct
Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.