Build Acuity Scheduling

KINDAreplaces $20/mosaves $240/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

AvailabilityComputed from calendar busy time, working hours and existing bookingsReservationSlots held inside the booking transaction, not checked then writtenTimeSlots stored as instants, displayed in the booker's zone and yoursPaymentsHosted checkout with deposits, completed on webhookNotificationsConfirmations and reminders through a queue with retries

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. free

    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

  4. free 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

  5. 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.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/bookingsClient bookings and intake responses, which may include health information.
CALENDAR_CLIENT_IDsecretrequireda provider client idCreate and update only the tool's own events.
HOLD_MINUTESrequired10Long enough to pay, short enough not to block a popular slot.
SYNC_STALE_MINUTESrequired30Beyond this availability narrows rather than being offered optimistically.
STRIPE_WEBHOOK_SECRETsecretrequiredwhsec_...Bookings confirm from webhooks, so an unverified endpoint books appointments.
TOUCH_FOREIGN_EVENTSrequiredneverPresent as a constant. There is no other value.

The build, in order

  1. Availability three sources agree on

    An offered slot is genuinely free.

    1. Calendar, rules and bookings. Any one alone offers a slot that is taken.

      Files src/availability.ts

    2. The series minus its cancellations, not the series.

      Files src/recurrence.tssrc/recurrence.test.ts

    3. Narrow availability rather than offering optimistically.

      Files src/availability.ts

    done when · tick each as it passes
    watch 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.
  2. Booking a slot exactly once

    Two people booking the last slot simultaneously produce one booking.

    1. Check-then-insert double-books when the page is busy.

      Files src/booking.ts

    2. Twenty parallel requests. Sequential tests prove nothing here.

      Files tests/concurrency.test.ts

    3. The client is already committed to booking something.

      Files src/booking.ts

    done when · tick each as it passes
    watch 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.
  3. Writing to your calendar, and nothing else

    A booking appears in your calendar and no real meeting is ever touched.

    1. A rename must not orphan a booking or endanger a meeting.

      Files src/calendar.ts

    2. In the write path, tested by attempting it.

      Files tests/foreign-events.test.ts

    3. Token expiry is routine and a duplicated week is very visible.

      Files src/sync.ts

    done when · tick each as it passes
    watch 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.
  4. Holds, deposits and payment

    A client filling in details does not lose the slot, and paying is not lost either.

    1. Abandoned checkouts never come back to release the slot.

      Files src/holds.ts

    2. Test by killing the browser after paying.

      Files src/webhooks.ts

    3. It happens, and double-booking is not the answer.

      Files src/holds.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.