IntermediateSchedulingMulti-tenancyTimezonesWebhooksOptimistic locking

Appointment SaaS

Multi-tenant scheduling with availability rules, holds, reminders, and calendar sync.

Product overview

Let service businesses publish availability and accept appointments without double booking across tenant rules, time zones, payments, and external calendars.

Main actors

Customer, Practitioner, Tenant admin, Receptionist.

Assumptions

  • Each organization is a tenant
  • Availability is computed from rules plus exceptions
  • External calendars are advisory until synchronized
  • All stored instants are UTC with an IANA zone for presentation

Non-goals

  • Medical records
  • Calendar provider replacement
  • Workforce payroll
  • Arbitrary resource optimization

Requirements

Must-have

  • Tenant-scoped services and staff
  • Recurring availability with exceptions
  • Race-safe booking
  • Cancellation/reschedule policy
  • Reminders

Should-have

  • Deposits
  • Group capacity
  • External calendar sync
  • Tenant booking page

Future scope

  • Waitlists
  • Round-robin assignment
  • Packages
  • Marketplace discovery

Non-functional decisions

ConcernDecision
ConsistencyBooking capacity is strongly enforced; reminders and external calendars converge.
Tenant isolationorganization_id participates in every ownership query and uniqueness boundary.
TimezonesRules use wall time plus IANA zone; concrete slots are UTC instants.
AvailabilityExisting schedules remain readable if calendar providers fail; sync status is explicit.
PrivacyCollect only scheduling details and restrict practitioner notes.

User roles

RoleAccess boundary
CustomerOwn appointments via account or signed management link
PractitionerOwn schedule and assigned appointments
ReceptionistTenant appointments and customers; limited settings
Tenant adminMembership, services, policies, integrations, audit

Core workflows

Book
Choose service and practitioner → Compute candidate slots → Create expiring slot hold → Collect deposit if required → Confirm appointment in transaction → Publish notification/calendar jobs
Reschedule
Authorize appointment → Hold new slot → Validate policy → Atomically move booking and release old capacity → Update calendars and notifications

Domain model

EntitiesRelationship
Organization → MembershipMembership establishes tenant role
Service → PractitionerEligibility and duration define bookable offering
AvailabilityRule + ExceptionRules generate candidates; exceptions block or add time
Appointment → ResourceBookingOne appointment reserves practitioner/room capacity
Appointment → ExternalCalendarLinkTracks provider version and sync state

Database design

PostgreSQL remains the transactional source of truth. Add specialized stores only for the workloads called out below.

availability_rules — Recurring local-time schedule

availability_rules ------------------ id UUID PK organization_id UUID FK practitioner_id UUID FK weekday SMALLINT start_local TIME end_local TIME timezone TEXT valid_from DATE Constraints: start_local < end_local Indexes: (organization_id, practitioner_id, valid_from)

appointments — Confirmed or held booking

appointments ------------ id UUID PK organization_id UUID FK practitioner_id UUID FK service_id UUID FK starts_at TIMESTAMPTZ ends_at TIMESTAMPTZ status TEXT hold_expires_at TIMESTAMPTZ version INT Constraints: ends_at > starts_at; exclusion constraint on active practitioner time range Indexes: (organization_id, practitioner_id, starts_at); (status, hold_expires_at)

notification_jobs — Durable scheduled reminders

notification_jobs ----------------- id UUID PK organization_id UUID appointment_id UUID FK kind TEXT run_at TIMESTAMPTZ status TEXT appointment_version INT Constraints: UNIQUE(appointment_id, kind, appointment_version) Indexes: (status, run_at)

Transaction and retention boundaries

  • PostgreSQL time-range exclusion constraint is final authority for a practitioner booking
  • Reschedule holds new capacity and releases old in one transaction
  • Appointment plus outbox commits together
  • Hard-delete expired unauthenticated holds; retain appointment/audit according to tenant policy

API design

GET /v1/booking/:tenant/services/:id/slots?from=&to= POST /v1/appointments/holds POST /v1/appointments/:id/confirm POST /v1/appointments/:id/reschedule GET /v1/appointments?cursor=…
EndpointPurpose and permissionIdempotency / status
GET /v1/booking/:tenant/services/:id/slots?from=&to=Return computed candidates
Public tenant page
Not required
200, 422
POST /v1/appointments/holdsTemporarily reserve a slot
Customer or receptionist
Required
201, 409, 422
POST /v1/appointments/:id/confirmConfirm held appointment
Hold owner
Required
200, 402, 409, 410
POST /v1/appointments/:id/rescheduleMove to a new slot
Customer link or tenant staff
Required
200, 409, 422
GET /v1/appointments?cursor=…Tenant schedule ordered by start/id
Tenant member
Not required
200, 403

List endpoints use opaque keyset cursors unless a small bounded collection is explicit. Unknown objects return 404 where 403 would leak existence.

Architecture

System context

Appointment SaaS system context
Appointment SaaS system context

Main application architecture

Appointment SaaS application architecture
Appointment SaaS application architecture

Critical workflow sequence

Appointment SaaS workflow sequence
Appointment SaaS workflow sequence

Event model

EventProducer → consumersDelivery
AppointmentConfirmedScheduling → Reminder, calendar syncOutbox; consumers key work by appointment/version
AppointmentRescheduledScheduling → Reminder cancellation, calendar syncNew version supersedes stale jobs
CalendarWebhookReceivedWebhook adapter → Sync workerSignature checked, provider event id deduped

Failure modes

Double booking

Failure
Two customers confirm the same slot.

Mitigation
Database exclusion/capacity constraint rejects one transaction.

Hold expires during payment

Failure
Deposit succeeds after slot release.

Mitigation
Use bounded hold and payment intent; reconcile late success into refund or manual resolution, never silent booking.

DST gap

Failure
Recurring 02:30 does not exist on transition day.

Mitigation
Timezone library resolves policy explicitly and tests transition dates.

Calendar echo

Failure
Our update returns as a provider webhook.

Mitigation
Store provider event id and our external event reference; suppress semantic duplicates.

Old reminder fires

Failure
Appointment was rescheduled after job creation.

Mitigation
Worker compares appointment version/status before send.

Signed link forwarded

Failure
Another person manages the booking.

Mitigation
Short-lived, purpose-scoped, revocable token; require account/verification for sensitive details.

Consistency and concurrency

Strong consistency / transactions

  • Practitioner/resource capacity
  • Hold conversion
  • Reschedule
  • deposit association

Eventual convergence

  • External calendars
  • reminders
  • availability read cache
  • analytics

Concurrency controls

  • Time-range exclusion constraint beats application-only slot checks
  • Optimistic version protects policy/status updates
  • Unique reminder version prevents duplicate send scheduling
  • Group bookings lock one capacity counter row

Caching and background work

Safe to cache

  • Service catalog
  • Availability rule versions
  • Short-lived generated slot candidates

Never a cache authority

  • Slot validity at confirm
  • Tenant membership
  • Deposit result

Cache candidate reads briefly by rule version; booking always attempts the database constraint.

Background jobs

  • Expire holds
  • Send version-checked reminders
  • Calendar push/pull sync
  • Reconcile deposits
  • Apply tenant retention

Security boundaries

Security claims here are design controls, not certification. Threat modeling and jurisdiction-specific review remain required.

  • Tenant scope derived from authenticated membership
  • Object authorization for appointment and practitioner
  • Signed booking links are scoped, expiring, and hashed at rest
  • Webhook signature/replay verification
  • Escape customer-entered text in notifications
  • Rate-limit public slot enumeration and booking
  • Keep sensitive notes out of calendar descriptions by default

Observability

Logs

  • Hold, confirm, reschedule, and cancel transitions with tenant, appointment version, and policy result
  • Timezone/rule version used to generate a selected slot
  • Calendar provider event id, echo suppression, and sync outcome

Metrics

  • Slot-query latency and confirmation conflict rate
  • Hold conversion/expiry and late-deposit resolution count
  • Reminder scheduling lateness and send suppression by stale version
  • Calendar sync lag, webhook duplication, and provider error rate

Traces

  • Trace slot discovery through range-constraint booking and deposit confirmation
  • Trace reschedule through new capacity reservation, old release, and provider sync

Alerts

  • Reminder queue is late enough to miss customer windows
  • Calendar sync lag exceeds tenant promise
  • Confirmed appointment count diverges from active capacity

Testing strategy

LayerWhat it proves
UnitRecurrence and DST fixtures, cancellation policy, appointment state machine
IntegrationPostgreSQL range conflicts, tenant boundaries, outbox
ContractCalendar/provider webhook payloads
ConcurrencyTwo confirms and concurrent reschedule
FailureLate payment, stale reminder, provider outage
End-to-endDiscover → hold → confirm → remind → reschedule/cancel

Do not mock PostgreSQL constraints, transaction isolation, object-store semantics, or provider contract fixtures in the tests intended to prove those boundaries.

Deployment evolution

Stage 1
Next.js, modular API, PostgreSQL, queue worker, email/calendar providers
Stage 2
API replicas, Redis candidate cache, provider-specific sync workers, read replica
Stage 3
Tenant cohorts and isolated enterprise databases where compliance/volume justify it

Scale evolution

These are architecture scenarios, not throughput claims. Each transition should follow measured workload and reliability pressure.

ScenarioArchitecture may evolve towardDecision rule
1K usersOne multi-tenant API, PostgreSQL with tenant_id on every row, a reminder worker, a calendar-sync workerPrevent double booking with an exclusion constraint on staff time ranges; store UTC instants alongside the tenant time zone
100K usersReplicate stateless API; cache candidate slots; isolate reminder and calendar workersCache computed availability briefly but recheck the constraint when booking; keep provider rate limits from delaying reminders
1M+ usersPartition tenants into cohorts; route a tenant to one booking writer and provide enterprise isolation where justifiedMove large or regulated tenants to dedicated cohorts; one booking writer per tenant keeps slot contention local