Building a Scheduling System: Complete Guide to Timestamp Management

Master timestamp handling in scheduling systems with timezone-aware events, recurring appointments, DST transitions, calendar integrations, and conflict detection. Production-ready patterns for booking systems, calendars, and appointment schedulers.

Building a Scheduling System: Timestamp Management

Ever tried to schedule a meeting across three timezones and had it somehow end up at 3 AM for someone? Yeah, scheduling systems are hard. They're probably the most timezone-sensitive applications you'll ever build. But here's the good news: once you understand the patterns, you can build schedulers that actually work reliably. Let's dive into the real-world solutions that power appointment systems, booking platforms, and calendar apps.

Core Challenges in Scheduling Systems

Let me paint you a picture. You're scheduling a meeting between someone in NYC and someone in Tokyo. "2 PM tomorrow" you say. But whose 2 PM? And what happens when DST kicks in next week? These aren't edge cases—they're Tuesday.

The Fundamental Problems (That'll Keep You Up at Night)

// Scenario: User in NYC schedules meeting with user in Tokyo
// Meeting: Tomorrow at 2 PM

// Question: 2 PM in whose timezone?
// - NYC: 2024-01-16 14:00 EST (UTC-5)
// - Tokyo: 2024-01-17 03:00 JST (UTC+9)  ← Next day!

// Challenge 1: Which timezone is the "source of truth"?
// Challenge 2: How to display to each participant?
// Challenge 3: What happens during DST transitions?
// Challenge 4: How to handle recurring events?

Key Requirements

  1. Timezone-Aware Storage: Store absolute moment (UTC) + display timezone
  2. User Context: Display in each user's local timezone
  3. DST Handling: Recurring events must respect DST transitions
  4. Conflict Detection: Check overlaps accounting for timezones
  5. Calendar Integration: Export/import with proper timezone data

Data Model Design

Event Schema

-- PostgreSQL schema for timezone-aware scheduling
CREATE TABLE events (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,

    -- Core timestamp fields
    start_at TIMESTAMPTZ NOT NULL,  -- Absolute moment (UTC)
    end_at TIMESTAMPTZ NOT NULL,

    -- Timezone context
    timezone VARCHAR(50) NOT NULL,  -- IANA timezone (e.g., "America/New_York")

    -- Original scheduling details
    scheduled_date DATE,            -- Date when event was intended
    scheduled_time TIME,            -- Time when event was intended

    -- Recurrence
    is_recurring BOOLEAN DEFAULT FALSE,
    recurrence_rule TEXT,           -- iCalendar RRULE format
    recurrence_end_date DATE,

    -- Metadata
    created_by_user_id INTEGER NOT NULL,
    location TEXT,
    description TEXT,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    CONSTRAINT valid_time_range CHECK (end_at > start_at)
);

-- Index for efficient range queries
CREATE INDEX idx_events_time_range ON events
    USING GIST (tstzrange(start_at, end_at));

-- Index for user's events
CREATE INDEX idx_events_user ON events (created_by_user_id);

-- Participants (many-to-many)
CREATE TABLE event_participants (
    id SERIAL PRIMARY KEY,
    event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
    user_id INTEGER NOT NULL,
    timezone VARCHAR(50) NOT NULL,  -- Participant's timezone
    status VARCHAR(20) DEFAULT 'pending',  -- pending, accepted, declined

    UNIQUE(event_id, user_id)
);

-- User timezone preferences
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    default_timezone VARCHAR(50) DEFAULT 'UTC',
    created_at TIMESTAMPTZ DEFAULT NOW()
);

TypeScript/JavaScript Model

// Domain models with Luxon
import { DateTime, Interval } from 'luxon';

interface Event {
  id: string;
  title: string;

  // Store as ISO strings in database
  startAt: string;  // ISO 8601 with timezone: "2024-01-16T14:00:00-05:00"
  endAt: string;

  timezone: string;  // IANA timezone

  // Original scheduling intent
  scheduledDate: string;  // "2024-01-16"
  scheduledTime: string;  // "14:00:00"

  isRecurring: boolean;
  recurrenceRule?: string;  // iCalendar RRULE

  createdBy: string;
  participants: Participant[];
}

interface Participant {
  userId: string;
  timezone: string;
  status: 'pending' | 'accepted' | 'declined';
}

// Convert to DateTime objects for manipulation
function parseEvent(event: Event): {
  start: DateTime;
  end: DateTime;
  interval: Interval;
} {
  const start = DateTime.fromISO(event.startAt);
  const end = DateTime.fromISO(event.endAt);

  return {
    start,
    end,
    interval: Interval.fromDateTimes(start, end),
  };
}

// Display event in user's timezone
function displayEventForUser(
  event: Event,
  userTimezone: string
): {
  localStart: string;
  localEnd: string;
  date: string;
  timeRange: string;
} {
  const { start, end } = parseEvent(event);

  const localStart = start.setZone(userTimezone);
  const localEnd = end.setZone(userTimezone);

  return {
    localStart: localStart.toISO(),
    localEnd: localEnd.toISO(),
    date: localStart.toFormat('MMMM d, yyyy'),
    timeRange: `${localStart.toFormat('h:mm a')} - ${localEnd.toFormat('h:mm a ZZZZ')}`,
  };
}

Creating Events

This is where the rubber meets the road. Creating a single event sounds simple, right? Parse the time, save it, done. Except... what about DST transitions? What about conflicts? What about that time someone tries to schedule a meeting at 2:30 AM on the day DST springs forward?

Single Event Creation (The Right Way)

import { DateTime } from 'luxon';

interface CreateEventInput {
  title: string;
  date: string;        // "2024-01-16"
  startTime: string;   // "14:00"
  endTime: string;     // "15:00"
  timezone: string;    // "America/New_York"
  userId: string;
}

async function createEvent(input: CreateEventInput): Promise<Event> {
  // Parse user's local time
  const startLocal = DateTime.fromFormat(
    `${input.date} ${input.startTime}`,
    'yyyy-MM-dd HH:mm',
    { zone: input.timezone }
  );

  const endLocal = DateTime.fromFormat(
    `${input.date} ${input.endTime}`,
    'yyyy-MM-dd HH:mm',
    { zone: input.timezone }
  );

  // Validate
  if (!startLocal.isValid || !endLocal.isValid) {
    throw new Error('Invalid date/time format');
  }

  if (endLocal <= startLocal) {
    throw new Error('End time must be after start time');
  }

  // Check for DST edge cases
  if (!startLocal.isValid) {
    throw new Error('Invalid time (possibly DST transition)');
  }

  // Store as ISO strings (includes timezone offset)
  const event: Event = {
    id: generateId(),
    title: input.title,
    startAt: startLocal.toISO(),  // "2024-01-16T14:00:00-05:00"
    endAt: endLocal.toISO(),
    timezone: input.timezone,
    scheduledDate: input.date,
    scheduledTime: input.startTime,
    isRecurring: false,
    createdBy: input.userId,
    participants: [],
  };

  // Check for conflicts
  const conflicts = await findConflicts(
    input.userId,
    startLocal,
    endLocal
  );

  if (conflicts.length > 0) {
    throw new ConflictError('Time slot already booked', conflicts);
  }

  // Save to database
  await db.events.create(event);

  return event;
}

Handling DST Transitions

import { DateTime } from 'luxon';

// Spring forward: March 10, 2024, 2:00 AM → 3:00 AM in NYC
// User tries to schedule 2:30 AM (doesn't exist)

function createEventSafeDST(
  date: string,
  time: string,
  timezone: string
): DateTime {
  const dt = DateTime.fromFormat(
    `${date} ${time}`,
    'yyyy-MM-dd HH:mm',
    { zone: timezone }
  );

  if (!dt.isValid) {
    // Check if it's a DST gap
    if (dt.invalidReason === 'unit out of range') {
      throw new Error(
        `Time ${time} does not exist on ${date} due to DST transition. ` +
        `The clock jumps forward at 2:00 AM.`
      );
    }
    throw new Error(`Invalid date/time: ${dt.invalidExplanation}`);
  }

  return dt;
}

// Fall back: November 3, 2024, 1:30 AM occurs twice
// Need to specify which occurrence

function createEventDuringFallback(
  date: string,
  time: string,
  timezone: string,
  isDST: boolean  // User specifies which occurrence
): DateTime {
  const dt = DateTime.fromFormat(
    `${date} ${time}`,
    'yyyy-MM-dd HH:mm',
    { zone: timezone }
  );

  // Luxon chooses first occurrence by default
  // Adjust if user wants second occurrence
  if (!isDST && dt.isInDST) {
    // Want standard time, but got DST
    return dt.plus({ hours: 1 });
  }

  return dt;
}

Recurring Events

iCalendar RRULE Standard

import { RRule, RRuleSet, rrulestr } from 'rrulejs';
import { DateTime } from 'luxon';

interface RecurrenceInput {
  frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
  interval: number;     // Every N days/weeks/months
  endDate?: string;     // When to stop
  count?: number;       // Or after N occurrences
  byWeekday?: number[]; // For weekly: [0=SU, 1=MO, ...]
  byMonthDay?: number;  // For monthly: day of month
}

function createRecurringEvent(
  baseEvent: CreateEventInput,
  recurrence: RecurrenceInput
): Event {
  const { start, end } = parseEventTimes(baseEvent);

  // Create RRULE
  const rule = new RRule({
    freq: getRRuleFrequency(recurrence.frequency),
    interval: recurrence.interval,
    dtstart: start.toJSDate(),
    until: recurrence.endDate
      ? DateTime.fromISO(recurrence.endDate).toJSDate()
      : undefined,
    count: recurrence.count,
    byweekday: recurrence.byWeekday,
    bymonthday: recurrence.byMonthDay,
    tzid: baseEvent.timezone,  // Critical for DST handling
  });

  const rruleString = rule.toString();

  return {
    ...baseEvent,
    isRecurring: true,
    recurrenceRule: rruleString,
    recurrenceEndDate: recurrence.endDate,
  };
}

// Generate occurrences for a date range
function generateOccurrences(
  event: Event,
  rangeStart: DateTime,
  rangeEnd: DateTime
): Array<{ start: DateTime; end: DateTime }> {
  if (!event.isRecurring || !event.recurrenceRule) {
    return [];
  }

  const rule = rrulestr(event.recurrenceRule, {
    dtstart: DateTime.fromISO(event.startAt).toJSDate(),
    tzid: event.timezone,
  });

  const duration = DateTime.fromISO(event.endAt)
    .diff(DateTime.fromISO(event.startAt));

  const occurrences = rule.between(
    rangeStart.toJSDate(),
    rangeEnd.toJSDate(),
    true  // inclusive
  );

  return occurrences.map(date => {
    const start = DateTime.fromJSDate(date, { zone: event.timezone });
    const end = start.plus(duration);

    return { start, end };
  });
}

// Example: Weekly team meeting, every Monday at 10 AM
const weeklyMeeting = createRecurringEvent(
  {
    title: 'Weekly Team Sync',
    date: '2024-01-15',  // Monday
    startTime: '10:00',
    endTime: '11:00',
    timezone: 'America/New_York',
    userId: 'user123',
  },
  {
    frequency: 'weekly',
    interval: 1,
    byWeekday: [1],  // Monday
    endDate: '2024-12-31',
  }
);

// Get next 5 occurrences
const nextOccurrences = generateOccurrences(
  weeklyMeeting,
  DateTime.now(),
  DateTime.now().plus({ months: 2 })
).slice(0, 5);

DST-Aware Recurring Events

import { DateTime } from 'luxon';

// Handle DST transitions in recurring events
function generateDSTAwareOccurrences(
  baseStart: DateTime,
  baseEnd: DateTime,
  timezone: string,
  dates: Date[]  // From RRULE
): Array<{ start: DateTime; end: DateTime; dstOffset: number }> {
  const duration = baseEnd.diff(baseStart);

  return dates.map(date => {
    // Create occurrence in original timezone
    const start = DateTime.fromJSDate(date, { zone: timezone });
    const end = start.plus(duration);

    return {
      start,
      end,
      dstOffset: start.offset,  // Track offset changes
    };
  });
}

// Example spanning DST transition
const recurringMeeting = {
  title: 'Weekly 9 AM Meeting',
  startAt: '2024-03-01T09:00:00-05:00',  // EST (UTC-5)
  endAt: '2024-03-01T10:00:00-05:00',
  timezone: 'America/New_York',
  recurrenceRule: 'FREQ=WEEKLY;BYDAY=FR',  // Every Friday
};

const occurrences = generateDSTAwareOccurrences(
  DateTime.fromISO(recurringMeeting.startAt),
  DateTime.fromISO(recurringMeeting.endAt),
  recurringMeeting.timezone,
  /* RRule generates dates */
);

// Results:
// March 1:  09:00-10:00 EST (UTC-5) ✓
// March 8:  09:00-10:00 EST (UTC-5) ✓
// March 15: 09:00-10:00 EDT (UTC-4) ✓ DST started March 10!
// Still 9 AM local time, but offset changed

Conflict Detection

Here's something that sounds simple but isn't: "Is this time slot already taken?" Turns out, detecting overlapping time intervals across timezones is surprisingly tricky. But we've got Luxon's Interval type to save us.

Overlap Algorithm (Don't Roll Your Own)

import { Interval } from 'luxon';

async function findConflicts(
  userId: string,
  start: DateTime,
  end: DateTime
): Promise<Event[]> {
  // Query events that might overlap
  const candidates = await db.events.find({
    createdBy: userId,
    // Efficient range query using PostgreSQL GIST index
    timeRange: {
      overlaps: { start: start.toISO(), end: end.toISO() }
    }
  });

  const newInterval = Interval.fromDateTimes(start, end);

  // Check for actual overlap
  const conflicts = candidates.filter(event => {
    const { interval } = parseEvent(event);
    return interval.overlaps(newInterval);
  });

  return conflicts;
}

// Check conflicts across multiple participants
async function findGroupConflicts(
  participantIds: string[],
  start: DateTime,
  end: DateTime
): Promise<Map<string, Event[]>> {
  const conflicts = new Map<string, Event[]>();

  await Promise.all(
    participantIds.map(async (userId) => {
      const userConflicts = await findConflicts(userId, start, end);
      if (userConflicts.length > 0) {
        conflicts.set(userId, userConflicts);
      }
    })
  );

  return conflicts;
}

Finding Available Slots

import { DateTime, Interval } from 'luxon';

interface TimeSlot {
  start: DateTime;
  end: DateTime;
}

async function findAvailableSlots(
  userId: string,
  date: string,
  timezone: string,
  slotDuration: number = 30,  // minutes
  businessHours: { start: string; end: string } = {
    start: '09:00',
    end: '17:00'
  }
): Promise<TimeSlot[]> {
  const dayStart = DateTime.fromFormat(
    `${date} ${businessHours.start}`,
    'yyyy-MM-dd HH:mm',
    { zone: timezone }
  );

  const dayEnd = DateTime.fromFormat(
    `${date} ${businessHours.end}`,
    'yyyy-MM-dd HH:mm',
    { zone: timezone }
  );

  // Get existing events for the day
  const events = await db.events.find({
    createdBy: userId,
    startAt: { gte: dayStart.toISO() },
    endAt: { lte: dayEnd.toISO() }
  });

  // Sort events by start time
  const sortedEvents = events
    .map(e => parseEvent(e))
    .sort((a, b) => a.start.toMillis() - b.start.toMillis());

  // Find gaps between events
  const availableSlots: TimeSlot[] = [];
  let currentTime = dayStart;

  for (const event of sortedEvents) {
    // Gap before this event?
    const gapDuration = event.start.diff(currentTime, 'minutes').minutes;

    if (gapDuration >= slotDuration) {
      availableSlots.push({
        start: currentTime,
        end: event.start
      });
    }

    // Move past this event
    currentTime = event.end;
  }

  // Gap after last event?
  const finalGap = dayEnd.diff(currentTime, 'minutes').minutes;
  if (finalGap >= slotDuration) {
    availableSlots.push({
      start: currentTime,
      end: dayEnd
    });
  }

  return availableSlots;
}

// Find common availability across multiple users
async function findCommonAvailability(
  userIds: string[],
  date: string,
  timezone: string,
  duration: number = 60  // minutes
): Promise<TimeSlot[]> {
  // Get available slots for each user
  const userSlots = await Promise.all(
    userIds.map(userId =>
      findAvailableSlots(userId, date, timezone, duration)
    )
  );

  // Find intersection of all availability
  return findSlotIntersection(userSlots, duration);
}

function findSlotIntersection(
  userSlots: TimeSlot[][],
  minDuration: number
): TimeSlot[] {
  if (userSlots.length === 0) return [];

  // Start with first user's slots
  let commonSlots = userSlots[0];

  // Intersect with each subsequent user
  for (let i = 1; i < userSlots.length; i++) {
    commonSlots = intersectSlots(commonSlots, userSlots[i]);
  }

  // Filter slots that meet minimum duration
  return commonSlots.filter(slot =>
    slot.end.diff(slot.start, 'minutes').minutes >= minDuration
  );
}

function intersectSlots(
  slotsA: TimeSlot[],
  slotsB: TimeSlot[]
): TimeSlot[] {
  const intersections: TimeSlot[] = [];

  for (const slotA of slotsA) {
    for (const slotB of slotsB) {
      const intervalA = Interval.fromDateTimes(slotA.start, slotA.end);
      const intervalB = Interval.fromDateTimes(slotB.start, slotB.end);

      const intersection = intervalA.intersection(intervalB);

      if (intersection && intersection.isValid) {
        intersections.push({
          start: intersection.start,
          end: intersection.end
        });
      }
    }
  }

  return mergeAdjacentSlots(intersections);
}

Calendar Integration

iCalendar Export

import ical from 'ical-generator';
import { DateTime } from 'luxon';

function exportToICalendar(events: Event[]): string {
  const calendar = ical({ name: 'My Calendar' });

  for (const event of events) {
    const { start, end } = parseEvent(event);

    const icalEvent = calendar.createEvent({
      start: start.toJSDate(),
      end: end.toJSDate(),
      summary: event.title,
      description: event.description,
      location: event.location,
      timezone: event.timezone,
    });

    // Add recurrence rule if applicable
    if (event.isRecurring && event.recurrenceRule) {
      icalEvent.repeating(event.recurrenceRule);
    }

    // Add participants
    for (const participant of event.participants) {
      icalEvent.createAttendee({
        email: participant.email,
        status: participant.status,
      });
    }
  }

  return calendar.toString();
}

// Google Calendar format
function exportToGoogleCalendar(event: Event): string {
  const { start, end } = parseEvent(event);

  const params = new URLSearchParams({
    action: 'TEMPLATE',
    text: event.title,
    dates: `${start.toFormat("yyyyMMdd'T'HHmmss")}/${end.toFormat("yyyyMMdd'T'HHmmss")}`,
    ctz: event.timezone,
    details: event.description || '',
    location: event.location || '',
  });

  return `https://calendar.google.com/calendar/render?${params.toString()}`;
}

Webhook Notifications

import { DateTime } from 'luxon';

interface ReminderConfig {
  eventId: string;
  userId: string;
  reminderMinutes: number[];  // [15, 60, 1440] = 15min, 1hr, 1day
}

async function scheduleReminders(config: ReminderConfig): Promise<void> {
  const event = await db.events.findById(config.eventId);
  const { start } = parseEvent(event);

  for (const minutes of config.reminderMinutes) {
    const reminderTime = start.minus({ minutes });

    // Schedule job (using Bull, Agenda, or similar)
    await queue.add(
      'send-reminder',
      {
        userId: config.userId,
        eventId: config.eventId,
        event: {
          title: event.title,
          start: start.toISO(),
          minutesUntil: minutes,
        }
      },
      {
        delay: reminderTime.diffNow().toMillis(),
        attempts: 3,
      }
    );
  }
}

// Handle reminder job
async function sendReminder(job: Job): Promise<void> {
  const { userId, event } = job.data;

  // Get user's current timezone
  const user = await db.users.findById(userId);
  const start = DateTime.fromISO(event.start).setZone(user.timezone);

  // Send notification in user's local time
  await notificationService.send({
    userId,
    title: `Upcoming: ${event.title}`,
    body: `Starts at ${start.toFormat('h:mm a ZZZZ')}`,
    data: { eventId: event.id },
  });
}

Real-World Examples

Example 1: Doctor Appointment Booking

interface Appointment {
  id: string;
  patientId: string;
  doctorId: string;
  appointmentType: 'consultation' | 'followup' | 'checkup';
  startAt: string;
  endAt: string;
  timezone: string;
  status: 'scheduled' | 'confirmed' | 'cancelled' | 'completed';
  notes?: string;
}

class AppointmentScheduler {
  async bookAppointment(
    patientId: string,
    doctorId: string,
    date: string,
    timeSlot: string,
    timezone: string
  ): Promise<Appointment> {
    // 1. Check doctor availability
    const doctor = await db.doctors.findById(doctorId);
    const workingHours = doctor.schedule[DateTime.fromISO(date).weekday];

    if (!this.isWithinWorkingHours(timeSlot, workingHours)) {
      throw new Error('Doctor not available at this time');
    }

    // 2. Parse time in doctor's timezone (clinic timezone)
    const clinicTz = doctor.clinicTimezone;
    const start = DateTime.fromFormat(
      `${date} ${timeSlot}`,
      'yyyy-MM-dd HH:mm',
      { zone: clinicTz }
    );

    const duration = this.getAppointmentDuration('consultation');
    const end = start.plus({ minutes: duration });

    // 3. Check conflicts
    const conflicts = await findConflicts(doctorId, start, end);
    if (conflicts.length > 0) {
      throw new ConflictError('Time slot already booked');
    }

    // 4. Create appointment
    const appointment: Appointment = {
      id: generateId(),
      patientId,
      doctorId,
      appointmentType: 'consultation',
      startAt: start.toISO(),
      endAt: end.toISO(),
      timezone: clinicTz,  // Store in clinic timezone
      status: 'scheduled',
    };

    await db.appointments.create(appointment);

    // 5. Send confirmations
    await this.sendConfirmations(appointment, timezone);

    // 6. Schedule reminders
    await scheduleReminders({
      eventId: appointment.id,
      userId: patientId,
      reminderMinutes: [1440, 60, 15],  // 1 day, 1 hour, 15 min
    });

    return appointment;
  }

  async sendConfirmations(
    appointment: Appointment,
    patientTimezone: string
  ): Promise<void> {
    const start = DateTime.fromISO(appointment.startAt);
    const patientStart = start.setZone(patientTimezone);

    await emailService.send({
      to: appointment.patientId,
      subject: 'Appointment Confirmed',
      body: `
        Your appointment is confirmed for:
        ${patientStart.toFormat('MMMM d, yyyy')}
        ${patientStart.toFormat('h:mm a ZZZZ')}

        Clinic timezone: ${start.toFormat('h:mm a ZZZZ')}
      `
    });
  }
}

Example 2: Multi-Timezone Meeting Scheduler

interface Meeting {
  id: string;
  title: string;
  organizerId: string;
  participants: Participant[];
  startAt: string;
  endAt: string;
  timezone: string;  // Organizer's timezone
}

class MeetingScheduler {
  async proposeMeetingTimes(
    participantIds: string[],
    duration: number,  // minutes
    dateRange: { start: string; end: string },
    preferredTimezone: string
  ): Promise<Array<{ time: DateTime; participantViews: Map<string, string> }>> {
    // 1. Get participant timezones
    const participants = await db.users.find({
      id: { in: participantIds }
    });

    // 2. Find common availability
    const proposals: Array<{
      time: DateTime;
      participantViews: Map<string, string>;
    }> = [];

    let currentDate = DateTime.fromISO(dateRange.start, {
      zone: preferredTimezone
    });

    const endDate = DateTime.fromISO(dateRange.end, {
      zone: preferredTimezone
    });

    while (currentDate < endDate) {
      const dateStr = currentDate.toISODate();

      // Find common slots for this day
      const commonSlots = await findCommonAvailability(
        participantIds,
        dateStr,
        preferredTimezone,
        duration
      );

      // Show time in each participant's timezone
      for (const slot of commonSlots) {
        const participantViews = new Map<string, string>();

        for (const participant of participants) {
          const localTime = slot.start.setZone(participant.timezone);
          participantViews.set(
            participant.id,
            localTime.toFormat('MMM d, h:mm a ZZZZ')
          );
        }

        proposals.push({
          time: slot.start,
          participantViews
        });
      }

      currentDate = currentDate.plus({ days: 1 });
    }

    return proposals;
  }

  async scheduleMeeting(
    title: string,
    organizerId: string,
    participantIds: string[],
    selectedTime: DateTime,
    duration: number
  ): Promise<Meeting> {
    const organizer = await db.users.findById(organizerId);
    const end = selectedTime.plus({ minutes: duration });

    // Check all participants for conflicts
    const conflicts = await findGroupConflicts(
      [organizerId, ...participantIds],
      selectedTime,
      end
    );

    if (conflicts.size > 0) {
      throw new ConflictError('Some participants have conflicts', conflicts);
    }

    // Create meeting
    const meeting: Meeting = {
      id: generateId(),
      title,
      organizerId,
      participants: participantIds.map(id => ({
        userId: id,
        timezone: participants.find(p => p.id === id)!.timezone,
        status: 'pending'
      })),
      startAt: selectedTime.toISO(),
      endAt: end.toISO(),
      timezone: organizer.timezone,
    };

    await db.meetings.create(meeting);

    // Send invitations
    await this.sendInvitations(meeting);

    return meeting;
  }
}

Best Practices

1. Always Store UTC + Timezone

// ✅ CORRECT
interface Event {
  startAt: string;   // "2024-01-16T19:00:00Z" (UTC)
  endAt: string;     // "2024-01-16T20:00:00Z" (UTC)
  timezone: string;  // "America/New_York" (for display)
}

// ❌ WRONG
interface Event {
  startAt: string;   // "2024-01-16T14:00:00" (no timezone!)
}

2. Validate Before DST Transitions

function validateEventTime(
  date: string,
  time: string,
  timezone: string
): { valid: boolean; message?: string } {
  const dt = DateTime.fromFormat(
    `${date} ${time}`,
    'yyyy-MM-dd HH:mm',
    { zone: timezone }
  );

  if (!dt.isValid) {
    return {
      valid: false,
      message: `Invalid time. ${dt.invalidExplanation}. ` +
               'This time may not exist due to DST transition.'
    };
  }

  return { valid: true };
}

3. Use Interval Overlap for Conflicts

// ✅ CORRECT: Use Interval.overlaps()
const interval1 = Interval.fromDateTimes(start1, end1);
const interval2 = Interval.fromDateTimes(start2, end2);
const hasConflict = interval1.overlaps(interval2);

// ❌ WRONG: Manual comparison (error-prone)
const hasConflict = start1 < end2 && start2 < end1;

4. Display in User's Timezone

function formatForUser(event: Event, userTimezone: string): string {
  const start = DateTime.fromISO(event.startAt).setZone(userTimezone);
  const end = DateTime.fromISO(event.endAt).setZone(userTimezone);

  return `${start.toFormat('MMM d, h:mm a')} - ${end.toFormat('h:mm a ZZZZ')}`;
}

The Bottom Line: Building Schedulers That Work

You know what's funny? I've built three different scheduling systems in my career, and every single time I thought, "This time I'll get it right from the start." Spoiler alert: I didn't. But each time, I learned what actually matters.

Here's what I wish someone had told me on day one:

The Non-Negotiables:

  1. UTC Storage - No exceptions. Ever. Store the absolute moment.
  2. Timezone Context - Keep the original timezone for display. Users care about "2 PM EST," not what that is in UTC.
  3. DST Handling - Don't try to be clever. Use Luxon (or your platform's equivalent) and let it handle transitions.
  4. Conflict Detection - Interval overlap is harder than you think. Use the Interval type. Trust me.
  5. Recurring Events - iCalendar's RRULE is battle-tested. Use it.
  6. User Display - Show each user their local time. Always.
  7. Calendar Integration - People will export to Google Calendar. Plan for it.
  8. Testing - Test around DST transitions. Seriously. March 10 and November 3 in NYC are your best friends.

The hardest part isn't the code—it's understanding that scheduling is fundamentally about coordinating different views of the same moment in time. Get that mental model right, and the rest falls into place.

Want my real advice? Build it, test it across timezones, watch it break in interesting ways, and iterate. That's how you get good at this.

Further Reading


Building a scheduling system? Contact us for consultation on datetime handling.