Skip to content

Specialist Scheduling Configuration

This document describes how specialists configure their availability for the public booking system. The scheduling configuration consists of four components:

  1. IANA timezone - The specialist's local timezone
  2. Weekly hours - Recurring availability blocks by day of week
  3. Date-specific overrides - Exceptions to the weekly schedule
  4. Active flag - Master switch for booking availability

Together, these components feed into the scheduling/availability engine which generates available timeslots for the public booking UI.


IANA Timezone

Every specialist has a scheduling_timezone field that stores their local timezone as an IANA timezone identifier (e.g., America/New_York, Europe/Bucharest, Asia/Tokyo).

Purpose

All scheduling calculations (weekly hours, overrides, appointment times) are performed in the specialist's local timezone. This ensures:

  • Specialists define their availability in their own wall-clock time
  • Patients see available timeslots converted to their local timezone in the booking UI
  • The system handles daylight saving time transitions automatically

Database Field

sql
-- On the specialists table
scheduling_timezone VARCHAR(64)

Null vs Set

  • When NULL: The specialist has no scheduling profile and cannot be booked through the public booking system. They may still be manually assigned to appointments by admins.
  • When set: The specialist's weekly hours and overrides become active. They appear in the booking flow for appointment types they're assigned to.

Valid Values

Must be a valid IANA timezone identifier. Examples:

  • America/New_York
  • America/Los_Angeles
  • Europe/London
  • Europe/Bucharest
  • Asia/Tokyo
  • Australia/Sydney

Validation: The API validates the timezone string against the IANA timezone database at creation/update time.

Changing Timezone

Timezone changes are blocked if appointment-type-specific overrides exist for the specialist. This prevents data corruption (overrides are stored as UTC timestamps, but interpreted in the specialist's timezone).

Workaround: Delete all overrides for the specialist before changing timezone, or create new overrides after the change.


Weekly Hours

Weekly hours define the specialist's recurring availability pattern. Each block specifies a day of week, start time, and end time in the specialist's local timezone.

Database Table

sql
CREATE TABLE specialist_weekly_hours (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    specialist_id   BIGINT NOT NULL REFERENCES specialists(id) ON DELETE CASCADE,
    day_of_week     day_of_week NOT NULL,  -- ENUM: mon, tue, wed, thu, fri, sat, sun
    start_time      TIME NOT NULL,
    end_time        TIME NOT NULL,

    CONSTRAINT uq_specialist_dow_start_end UNIQUE (specialist_id, day_of_week, start_time, end_time)
);

Examples

A specialist who works Monday-Friday 9am-5pm with a lunch break 12pm-1pm:

Day of WeekStart TimeEnd Time
mon09:00:0012:00:00
mon13:00:0017:00:00
tue09:00:0012:00:00
tue13:00:0017:00:00
wed09:00:0012:00:00
wed13:00:0017:00:00
thu09:00:0012:00:00
thu13:00:0017:00:00
fri09:00:0012:00:00
fri13:00:0017:00:00

Multiple blocks per day: A specialist can have multiple blocks on the same day (e.g., morning and afternoon shifts with a break).

No blocks for a day: If no weekly hours are defined for a day (e.g., Saturday and Sunday), the specialist is unavailable on that day by default.

Time Format

Times are stored as PostgreSQL TIME (wall-clock time, no timezone offset). They are always interpreted in the specialist's scheduling_timezone.

  • Format: HH:MM:SS (24-hour)
  • Examples: 09:00:00, 13:30:00, 17:45:00

API Endpoints

Create Weekly Hours Block

http
POST /v1/specialists/{id}/weekly-hours
json
{
  "dayOfWeek": "mon",
  "startTime": "09:00:00",
  "endTime": "12:00:00"
}

List Weekly Hours

http
GET /v1/specialists/{id}/weekly-hours
json
{
  "data": [
    {
      "id": "uuid",
      "dayOfWeek": "mon",
      "startTime": "09:00:00",
      "endTime": "12:00:00"
    }
  ]
}

Update Weekly Hours Block

http
PATCH /v1/specialists/{id}/weekly-hours/{weeklyHourId}
json
{
  "startTime": "08:00:00",
  "endTime": "12:00:00"
}

Delete Weekly Hours Block

http
DELETE /v1/specialists/{id}/weekly-hours/{weeklyHourId}

Bulk Replace All Weekly Hours

http
POST /v1/specialists/{id}/weekly-hours/bulk-replace
json
{
  "replacements": [
    {
      "dayOfWeek": "mon",
      "slots": [
        { "startTime": "09:00:00", "endTime": "12:00:00" },
        { "startTime": "14:00:00", "endTime": "18:00:00" }
      ]
    },
    {
      "dayOfWeek": "tue",
      "slots": [
        { "startTime": "09:00:00", "endTime": "17:00:00" }
      ]
    }
  ]
}

Date-Specific Overrides

Overrides are exceptions to the weekly schedule. They allow specialists to:

  • Block availability on specific dates (vacations, holidays, sick days)
  • Add availability on days they don't normally work (extra hours, special events)
  • Replace availability for specific dates (different hours than usual)

Database Table

sql
CREATE TABLE specialist_schedule_overrides (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    appointment_type_id UUID NOT NULL REFERENCES appointment_types(id) ON DELETE CASCADE,
    specialist_id       BIGINT NOT NULL REFERENCES specialists(id) ON DELETE CASCADE,
    start_date          TIMESTAMPTZ NOT NULL,
    end_date            TIMESTAMPTZ NOT NULL,
    availability        BOOLEAN NOT NULL,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Scoping

Overrides are scoped to a specific appointment type + specialist pair. This means:

  • A specialist can block availability for appointment type A while remaining available for appointment type B
  • Different appointment types can have different override rules for the same specialist
  • Useful for managing complex scheduling scenarios (e.g., "No new patient consults this week, but follow-ups are OK")

Availability Flag

The availability boolean determines the override behavior:

  • true (add/replace): This override adds availability for the specified date range. The weekly hours are ignored for affected days. The override defines new available time blocks.
  • false (block): This override blocks availability for the specified date range. No timeslots are generated, even if weekly hours would normally apply.

Date Range Format

Overrides use TIMESTAMPTZ (timestamps with timezone) for start and end dates. They are stored in UTC but interpreted in the specialist's timezone.

Example: Block availability for vacation

json
{
  "appointmentTypeId": "abc-123-uuid",
  "startDate": "2025-12-20T00:00:00Z",
  "endDate": "2025-12-31T23:59:59Z",
  "availability": false
}

How Overrides Work

When the availability engine generates timeslots:

  1. Check for blocking overrides first: If any override with availability=false covers the date, skip this day entirely.
  2. Check for additive overrides: If any override with availability=true covers the date, use the override's time range instead of weekly hours.
  3. Fall back to weekly hours: If no overrides apply, use the weekly hours for that day of week.

Priority: Overrides always take precedence over weekly hours.

Examples

Block Vacation (Dec 20-31, 2025)

json
{
  "appointmentTypeId": "abc-123-uuid",
  "startDate": "2025-12-20T00:00:00Z",
  "endDate": "2025-12-31T23:59:59Z",
  "availability": false
}

Effect: No timeslots generated Dec 20-31 for this appointment type, even if weekly hours would normally apply.

Add Extra Hours on Saturday

json
{
  "appointmentTypeId": "abc-123-uuid",
  "startDate": "2025-03-15T14:00:00Z",
  "endDate": "2025-03-15T20:00:00Z",
  "availability": true
}

Effect: On March 15, 2025 (a Saturday where the specialist normally doesn't work), timeslots are generated from 2pm-8pm (in the specialist's timezone).

Replace Hours for a Single Day

json
{
  "appointmentTypeId": "abc-123-uuid",
  "startDate": "2025-04-10T12:00:00Z",
  "endDate": "2025-04-10T16:00:00Z",
  "availability": true
}

Effect: On April 10, 2025, ignore weekly hours and only generate timeslots from 12pm-4pm.

API Endpoints

List Overrides

http
GET /v1/specialists/{id}/overrides?appointmentTypeId={uuid}
json
{
  "data": [
    {
      "id": "uuid",
      "appointmentTypeId": "uuid",
      "startDate": "2025-12-20T00:00:00Z",
      "endDate": "2025-12-31T23:59:59Z",
      "availability": false
    }
  ]
}

Create Override

http
POST /v1/specialists/{id}/overrides
json
{
  "appointmentTypeId": "uuid",
  "startDate": "2025-12-20T00:00:00Z",
  "endDate": "2025-12-31T23:59:59Z",
  "availability": false
}

Update Override

http
PATCH /v1/specialists/{id}/overrides/{overrideId}
json
{
  "startDate": "2025-12-21T00:00:00Z",
  "endDate": "2025-12-30T23:59:59Z"
}

Delete Override

http
DELETE /v1/specialists/{id}/overrides/{overrideId}?appointmentTypeId={uuid}

Bulk Create Overrides

http
POST /v1/specialists/{id}/overrides/bulk-create
json
{
  "overrides": [
    {
      "appointmentTypeId": "uuid",
      "startDate": "2025-12-20T00:00:00Z",
      "endDate": "2025-12-31T23:59:59Z",
      "availability": false
    },
    {
      "appointmentTypeId": "uuid",
      "startDate": "2026-01-15T14:00:00Z",
      "endDate": "2026-01-15T20:00:00Z",
      "availability": true
    }
  ]
}

Cleanup Expired Overrides

http
DELETE /v1/specialists/{id}/overrides/cleanup?strategy=moderate

Strategies:

  • aggressive: Delete all expired overrides (0 days retention)
  • moderate: Keep overrides for 60 days after expiry (default)
  • conservative: Keep overrides for 365 days after expiry

Active Flag

The scheduling_active boolean on the specialists table is a master switch for booking availability.

Database Field

sql
-- On the specialists table
scheduling_active BOOLEAN DEFAULT TRUE

Behavior

  • When true: The specialist appears in booking flows for appointment types they're assigned to (if scheduling_timezone is also set).
  • When false: The specialist does not appear in public booking flows, even if they have a timezone and valid weekly hours. Useful for temporarily disabling bookings without deleting scheduling configuration.

Use Cases

  • Temporary leave: Set scheduling_active = false when a specialist goes on leave. Set back to true when they return.
  • Onboarding: Create a specialist with full scheduling configuration but keep scheduling_active = false until they're ready to accept bookings.
  • Deactivation: Deactivate a specialist without soft-deleting their record.

How Scheduling Config Feeds Into the Availability Engine

The scheduling configuration (timezone, weekly hours, overrides, active flag) is consumed by the scheduling/availability engine to generate available timeslots for the public booking UI.

High-Level Flow

  1. Frontend requests timeslots: GET /v1/appointment-types/{id}/timeslots
  2. Availability engine queries scheduling config:
    • Fetch appointment type with slotDurationMinutes, slotGapMinutes, slotsHorizonDays
    • Fetch all specialists assigned to this appointment type with scheduling_active = true
    • For each specialist, fetch scheduling_timezone, weekly hours, and overrides
  3. Generate timeslots for each day in the horizon:
    • Check for blocking overrides (availability=false) → skip this day
    • Check for additive overrides (availability=true) → use override time range
    • Fall back to weekly hours for this day of week
    • Slice the available time into slots (duration + gap)
    • Convert slots from specialist's timezone to UTC
    • Filter out past slots, already-booked slots, held slots
  4. Pool timeslots across all specialists (if not filtered by specialistId)
  5. Cache results in Redis (5 min default TTL)
  6. Return timeslots grouped by day

Detailed Processing Logic

For each day in the booking horizon:

python
# Pseudocode
for date in range(today, today + horizon_days):
    # 1. Check for blocking overrides
    blocking_overrides = get_overrides(specialist_id, appointment_type_id, date, availability=false)
    if blocking_overrides:
        continue  # Skip this day

    # 2. Check for additive overrides
    additive_overrides = get_overrides(specialist_id, appointment_type_id, date, availability=true)
    if additive_overrides:
        # Use override time range (replaces weekly hours for this day)
        time_blocks = [(override.start_time, override.end_time) for override in additive_overrides]
    else:
        # 3. Fall back to weekly hours
        day_of_week = date.weekday()  # mon, tue, wed, etc.
        weekly_hours = get_weekly_hours(specialist_id, day_of_week)
        time_blocks = [(wh.start_time, wh.end_time) for wh in weekly_hours]

    # 4. Slice time blocks into slots
    for block in time_blocks:
        current_time = block.start_time
        while current_time + slot_duration <= block.end_time:
            slot_start = combine(date, current_time, specialist_timezone)
            slot_end = slot_start + slot_duration
            slots.append((slot_start, slot_end))
            current_time += slot_duration + slot_gap

    # 5. Convert to UTC, filter past/booked/held slots
    slots = [s for s in slots if s.start > now and not is_booked(s) and not is_held(s)]

Priority-Based Specialist Assignment

When a patient books an appointment without selecting a specific specialist, the system uses priority-based assignment:

  1. Fetch all specialists assigned to the appointment type, ordered by priority (higher priority first)
  2. For each specialist in priority order, check if they have availability for the requested timeslot
  3. Assign the first specialist with availability
  4. If no specialist has availability, return an error

See ../scheduling/ for full details on the availability engine, caching strategy, and hold system.


  • schema.sql - Database schema for specialists and scheduling tables
  • api.md - API endpoints for managing specialists, weekly hours, and overrides
  • ../scheduling/ - Full scheduling feature documentation (availability engine, timeslots, holds, booking flow)