Timezone Handling
Timezone handling is one of the hardest parts of scheduling software. The Booking Kit follows a simple principle: store and compute in UTC, display in local time.
Core utilities
Section titled “Core utilities”import { normalizeToUTC, utcToLocal, isValidTimezone, getTimezoneOffset, InvalidTimezoneError,} from "@thebookingkit/core";normalizeToUTC(localTime, timezone)
Section titled “normalizeToUTC(localTime, timezone)”Converts a local time string to a UTC Date:
const utc = normalizeToUTC("2026-03-10T09:00:00", "America/New_York");// Date: 2026-03-10T14:00:00.000Z (EST is UTC-5)utcToLocal(utcDate, timezone)
Section titled “utcToLocal(utcDate, timezone)”Converts a UTC Date to a local time string:
const local = utcToLocal(new Date("2026-03-10T14:00:00Z"), "America/New_York");// "2026-03-10T09:00:00"isValidTimezone(timezone)
Section titled “isValidTimezone(timezone)”Validates an IANA timezone string:
isValidTimezone("America/New_York"); // trueisValidTimezone("US/Eastern"); // trueisValidTimezone("EST"); // false (abbreviation, not IANA)isValidTimezone("Invalid/Zone"); // falsegetTimezoneOffset(timezone, date)
Section titled “getTimezoneOffset(timezone, date)”Returns the UTC offset in minutes for a timezone at a specific date:
getTimezoneOffset("America/New_York", new Date("2026-03-10")); // -300 (EST)getTimezoneOffset("America/New_York", new Date("2026-07-10")); // -240 (EDT)How timezones flow through the system
Section titled “How timezones flow through the system”1. Rules are defined in provider timezone
Section titled “1. Rules are defined in provider timezone”{ rrule: "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", startTime: "09:00", endTime: "17:00", timezone: "America/New_York", // Provider's timezone}2. Slots are computed in UTC
Section titled “2. Slots are computed in UTC”The slot engine converts rule times to UTC before computing availability. All overlap checks, buffer calculations, and booking comparisons happen in UTC.
3. Slots are displayed in customer timezone
Section titled “3. Slots are displayed in customer timezone”The customerTimezone parameter controls the localStart and localEnd fields on each slot:
const slots = getAvailableSlots(rules, overrides, bookings, dateRange, "Asia/Tokyo");
// slots[0].startTime → "2026-03-10T14:00:00.000Z" (UTC)// slots[0].localStart → "11:00 PM" (JST, UTC+9)4. Bookings are stored in UTC
Section titled “4. Bookings are stored in UTC”The bookings table stores starts_at and ends_at as UTC timestamps. This ensures correct behavior regardless of DST changes between booking creation and the appointment date.
Common pitfall: dateRange must be UTC
Section titled “Common pitfall: dateRange must be UTC”The dateRange parameter passed to getAvailableSlots() must use UTC Date objects. The RRULE expansion uses dateRange.start as dtstart, which determines the time-of-day reference for generated occurrences. Passing local-time dates on a non-UTC server silently shifts the boundary and can exclude an entire day’s occurrences.
// ✅ Correct — explicit UTC with Z suffixconst dateRange = { start: new Date("2026-03-09T00:00:00.000Z"), end: new Date("2026-03-09T23:59:59.999Z"),};
// ✅ Also correct — Date.UTC()const dateRange = { start: new Date(Date.UTC(2026, 2, 9, 0, 0, 0)), end: new Date(Date.UTC(2026, 2, 9, 23, 59, 59, 999)),};
// ❌ WRONG — parsed as server's local time, breaks on non-UTC serversconst dateRange = { start: new Date("2026-03-09T00:00:00"), // no Z! end: new Date("2026-03-09T23:59:59"),};Why it breaks: On a server running in Australia/Sydney (UTC+11), new Date("2026-03-09T00:00:00") becomes 2026-03-08T13:00:00Z. The RRULE generates Monday’s occurrence at 13:00 UTC on March 9 — but dateRange.end is 12:59:59 UTC, so the entire day is excluded.
DST transitions
Section titled “DST transitions”The Booking Kit handles DST transitions correctly:
- Spring forward: If a provider is available 9am-5pm Eastern and DST springs forward on March 8, slots on March 9 correctly map to UTC-4 instead of UTC-5
- Fall back: The extra hour during fall-back does not create duplicate slots
- Recurring bookings: Weekly series maintain the same local time across DST boundaries (e.g., “every Monday at 9am” stays at 9am local time)
Libraries used
Section titled “Libraries used”The Booking Kit uses date-fns and date-fns-tz for all date/time operations:
fromZonedTime()— Convert local time to UTCtoZonedTime()— Convert UTC to local timeformat()— Format dates in a specific timezone
These are peer dependencies of @thebookingkit/core.