Specialist Scheduling Configuration
This document describes how specialists configure their availability for the public booking system. The scheduling configuration consists of four components:
- IANA timezone - The specialist's local timezone
- Weekly hours - Recurring availability blocks by day of week
- Date-specific overrides - Exceptions to the weekly schedule
- 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
-- 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_YorkAmerica/Los_AngelesEurope/LondonEurope/BucharestAsia/TokyoAustralia/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
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 Week | Start Time | End Time |
|---|---|---|
mon | 09:00:00 | 12:00:00 |
mon | 13:00:00 | 17:00:00 |
tue | 09:00:00 | 12:00:00 |
tue | 13:00:00 | 17:00:00 |
wed | 09:00:00 | 12:00:00 |
wed | 13:00:00 | 17:00:00 |
thu | 09:00:00 | 12:00:00 |
thu | 13:00:00 | 17:00:00 |
fri | 09:00:00 | 12:00:00 |
fri | 13:00:00 | 17: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
POST /v1/specialists/{id}/weekly-hours{
"dayOfWeek": "mon",
"startTime": "09:00:00",
"endTime": "12:00:00"
}List Weekly Hours
GET /v1/specialists/{id}/weekly-hours{
"data": [
{
"id": "uuid",
"dayOfWeek": "mon",
"startTime": "09:00:00",
"endTime": "12:00:00"
}
]
}Update Weekly Hours Block
PATCH /v1/specialists/{id}/weekly-hours/{weeklyHourId}{
"startTime": "08:00:00",
"endTime": "12:00:00"
}Delete Weekly Hours Block
DELETE /v1/specialists/{id}/weekly-hours/{weeklyHourId}Bulk Replace All Weekly Hours
POST /v1/specialists/{id}/weekly-hours/bulk-replace{
"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
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
{
"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:
- Check for blocking overrides first: If any override with
availability=falsecovers the date, skip this day entirely. - Check for additive overrides: If any override with
availability=truecovers the date, use the override's time range instead of weekly hours. - 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)
{
"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
{
"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
{
"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
GET /v1/specialists/{id}/overrides?appointmentTypeId={uuid}{
"data": [
{
"id": "uuid",
"appointmentTypeId": "uuid",
"startDate": "2025-12-20T00:00:00Z",
"endDate": "2025-12-31T23:59:59Z",
"availability": false
}
]
}Create Override
POST /v1/specialists/{id}/overrides{
"appointmentTypeId": "uuid",
"startDate": "2025-12-20T00:00:00Z",
"endDate": "2025-12-31T23:59:59Z",
"availability": false
}Update Override
PATCH /v1/specialists/{id}/overrides/{overrideId}{
"startDate": "2025-12-21T00:00:00Z",
"endDate": "2025-12-30T23:59:59Z"
}Delete Override
DELETE /v1/specialists/{id}/overrides/{overrideId}?appointmentTypeId={uuid}Bulk Create Overrides
POST /v1/specialists/{id}/overrides/bulk-create{
"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
DELETE /v1/specialists/{id}/overrides/cleanup?strategy=moderateStrategies:
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
-- On the specialists table
scheduling_active BOOLEAN DEFAULT TRUEBehavior
- When
true: The specialist appears in booking flows for appointment types they're assigned to (ifscheduling_timezoneis 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 = falsewhen a specialist goes on leave. Set back totruewhen they return. - Onboarding: Create a specialist with full scheduling configuration but keep
scheduling_active = falseuntil 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
- Frontend requests timeslots:
GET /v1/appointment-types/{id}/timeslots - 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
- Fetch appointment type with
- 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
- Check for blocking overrides (
- Pool timeslots across all specialists (if not filtered by
specialistId) - Cache results in Redis (5 min default TTL)
- Return timeslots grouped by day
Detailed Processing Logic
For each day in the booking horizon:
# 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:
- Fetch all specialists assigned to the appointment type, ordered by priority (higher priority first)
- For each specialist in priority order, check if they have availability for the requested timeslot
- Assign the first specialist with availability
- If no specialist has availability, return an error
See ../scheduling/ for full details on the availability engine, caching strategy, and hold system.
Related Documentation
- 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)