πŸ“š Online Appointment Scheduling 24 min read

How to Build an Online Appointment Scheduling System: Complete Guide with Production-Ready Code

Step-by-step guide to building a HIPAA-compliant appointment scheduling system with calendar integration, automated reminders, time zone handling, provider availability management, and waitlist functionality. Includes complete React, Node.js, and PostgreSQL code examples.

✍️
Dr. Sarah Chen

How to Build an Online Appointment Scheduling System: Complete Guide with Production-Ready Code

Building a healthcare appointment scheduling system requires careful attention to user experience, data integrity, HIPAA compliance, and operational efficiency. This comprehensive guide walks you through building a production-ready scheduling platform with calendar integration, automated reminders, time zone handling, provider management, and waitlist functionality.

Time to Build: 6-12 months (traditional approach) or 2-4 weeks with JustCopy.ai

Tech Stack: React, Node.js, PostgreSQL, Redis, AWS

Key Features: Real-time availability, SMS/email reminders, calendar sync, waitlist automation, EHR integration

System Architecture Overview

A production-grade appointment scheduling system requires multiple integrated components:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Frontend Layer                           β”‚
β”‚  React SPA + Mobile PWA + Provider Dashboard                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      API Gateway                             β”‚
β”‚  Authentication + Rate Limiting + Load Balancing             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Core Services Layer                        β”‚
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚  Scheduling  β”‚  β”‚   Provider   β”‚  β”‚   Patient    β”‚     β”‚
β”‚  β”‚   Service    β”‚  β”‚   Service    β”‚  β”‚   Service    β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚   Reminder   β”‚  β”‚   Waitlist   β”‚  β”‚   Calendar   β”‚     β”‚
β”‚  β”‚   Service    β”‚  β”‚   Service    β”‚  β”‚   Service    β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Data Layer                                β”‚
β”‚  PostgreSQL + Redis Cache + S3 Storage                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                External Integrations                         β”‚
β”‚  Twilio SMS + SendGrid Email + Calendar APIs + EHR Systems   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Rather than building this entire architecture from scratchβ€”which typically requires 6-12 months and $150K-$500Kβ€”you can use JustCopy.ai to deploy a proven, production-ready scheduling platform in weeks. The platform’s 10 specialized AI agents handle code generation, automated testing, deployment pipelines, and monitoring, all while ensuring HIPAA compliance and healthcare industry best practices.

Part 1: Database Schema Design

A robust scheduling system requires carefully designed database schemas:

Core Tables

-- Database schema for appointment scheduling system
-- PostgreSQL 14+

-- Patients table
CREATE TABLE patients (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    phone VARCHAR(20) NOT NULL,
    date_of_birth DATE NOT NULL,

    -- Address
    address_line1 VARCHAR(255),
    address_line2 VARCHAR(255),
    city VARCHAR(100),
    state VARCHAR(2),
    zip_code VARCHAR(10),

    -- Preferences
    preferred_language VARCHAR(10) DEFAULT 'en',
    communication_preferences JSONB DEFAULT '{"sms": true, "email": true, "phone": false}'::jsonb,
    timezone VARCHAR(50) DEFAULT 'America/New_York',

    -- Insurance
    insurance_provider VARCHAR(255),
    insurance_member_id VARCHAR(100),
    insurance_group_number VARCHAR(100),

    -- Medical
    primary_care_provider_id UUID REFERENCES providers(id),
    medical_conditions TEXT[],
    allergies TEXT[],
    medications JSONB,

    -- Analytics
    total_appointments INTEGER DEFAULT 0,
    completed_appointments INTEGER DEFAULT 0,
    no_shows INTEGER DEFAULT 0,
    cancellations INTEGER DEFAULT 0,

    -- Metadata
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    last_appointment_at TIMESTAMP WITH TIME ZONE,

    -- HIPAA compliance
    consent_to_treat BOOLEAN DEFAULT FALSE,
    consent_to_sms BOOLEAN DEFAULT FALSE,
    consent_to_email BOOLEAN DEFAULT FALSE,
    hipaa_authorization_signed BOOLEAN DEFAULT FALSE,

    -- Soft delete
    deleted_at TIMESTAMP WITH TIME ZONE
);

CREATE INDEX idx_patients_email ON patients(email) WHERE deleted_at IS NULL;
CREATE INDEX idx_patients_phone ON patients(phone) WHERE deleted_at IS NULL;
CREATE INDEX idx_patients_last_name ON patients(last_name) WHERE deleted_at IS NULL;

-- Providers table
CREATE TABLE providers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    npi VARCHAR(10) UNIQUE NOT NULL,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    credentials VARCHAR(50),

    specialty VARCHAR(100) NOT NULL,
    sub_specialty VARCHAR(100),

    email VARCHAR(255) UNIQUE NOT NULL,
    phone VARCHAR(20),

    bio TEXT,
    photo_url VARCHAR(500),

    -- Scheduling preferences
    default_appointment_duration INTEGER DEFAULT 30,
    buffer_time_minutes INTEGER DEFAULT 5,
    max_advance_booking_days INTEGER DEFAULT 90,
    min_advance_booking_hours INTEGER DEFAULT 24,

    -- Online booking settings
    accepts_online_booking BOOLEAN DEFAULT TRUE,
    accepts_new_patients BOOLEAN DEFAULT TRUE,
    max_online_bookings_per_day INTEGER DEFAULT 10,

    -- Status
    status VARCHAR(20) DEFAULT 'ACTIVE',

    -- Metadata
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    -- Soft delete
    deleted_at TIMESTAMP WITH TIME ZONE
);

CREATE INDEX idx_providers_specialty ON providers(specialty) WHERE deleted_at IS NULL;
CREATE INDEX idx_providers_status ON providers(status) WHERE deleted_at IS NULL;

-- Locations table
CREATE TABLE locations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,

    address_line1 VARCHAR(255) NOT NULL,
    address_line2 VARCHAR(255),
    city VARCHAR(100) NOT NULL,
    state VARCHAR(2) NOT NULL,
    zip_code VARCHAR(10) NOT NULL,

    phone VARCHAR(20) NOT NULL,
    fax VARCHAR(20),

    -- Geolocation for distance calculations
    latitude DECIMAL(10, 8),
    longitude DECIMAL(11, 8),

    -- Operating hours
    hours_of_operation JSONB DEFAULT '{
        "monday": {"open": "08:00", "close": "17:00"},
        "tuesday": {"open": "08:00", "close": "17:00"},
        "wednesday": {"open": "08:00", "close": "17:00"},
        "thursday": {"open": "08:00", "close": "17:00"},
        "friday": {"open": "08:00", "close": "17:00"},
        "saturday": null,
        "sunday": null
    }'::jsonb,

    timezone VARCHAR(50) DEFAULT 'America/New_York',

    -- Facilities
    parking_available BOOLEAN DEFAULT TRUE,
    wheelchair_accessible BOOLEAN DEFAULT TRUE,

    status VARCHAR(20) DEFAULT 'ACTIVE',

    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP WITH TIME ZONE
);

CREATE INDEX idx_locations_city_state ON locations(city, state) WHERE deleted_at IS NULL;
CREATE INDEX idx_locations_coords ON locations USING GIST (
    ll_to_earth(latitude, longitude)
) WHERE deleted_at IS NULL AND latitude IS NOT NULL AND longitude IS NOT NULL;

-- Provider availability schedules
CREATE TABLE provider_schedules (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    provider_id UUID NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
    location_id UUID NOT NULL REFERENCES locations(id),

    -- Day of week (0 = Sunday, 6 = Saturday)
    day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6),

    -- Time blocks
    start_time TIME NOT NULL,
    end_time TIME NOT NULL,

    -- Effective dates
    effective_from DATE NOT NULL,
    effective_until DATE,

    -- Appointment types allowed during this block
    allowed_appointment_types UUID[] DEFAULT ARRAY[]::UUID[],

    -- Recurrence
    is_recurring BOOLEAN DEFAULT TRUE,

    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT valid_time_range CHECK (end_time > start_time)
);

CREATE INDEX idx_provider_schedules_provider ON provider_schedules(provider_id);
CREATE INDEX idx_provider_schedules_location ON provider_schedules(location_id);
CREATE INDEX idx_provider_schedules_day ON provider_schedules(day_of_week);

-- Provider schedule exceptions (PTO, holidays, etc.)
CREATE TABLE schedule_exceptions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    provider_id UUID NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
    location_id UUID REFERENCES locations(id),

    exception_type VARCHAR(50) NOT NULL, -- 'PTO', 'HOLIDAY', 'CONFERENCE', 'EMERGENCY', etc.

    start_datetime TIMESTAMP WITH TIME ZONE NOT NULL,
    end_datetime TIMESTAMP WITH TIME ZONE NOT NULL,

    all_day BOOLEAN DEFAULT FALSE,

    reason TEXT,

    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    created_by UUID REFERENCES providers(id),

    CONSTRAINT valid_datetime_range CHECK (end_datetime > start_datetime)
);

CREATE INDEX idx_schedule_exceptions_provider ON schedule_exceptions(provider_id);
CREATE INDEX idx_schedule_exceptions_dates ON schedule_exceptions(start_datetime, end_datetime);

-- Appointment types
CREATE TABLE appointment_types (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(100) NOT NULL,
    description TEXT,

    default_duration_minutes INTEGER NOT NULL,

    -- Booking rules
    requires_referral BOOLEAN DEFAULT FALSE,
    new_patients_allowed BOOLEAN DEFAULT TRUE,
    online_booking_allowed BOOLEAN DEFAULT TRUE,

    -- Categorization
    category VARCHAR(50), -- 'CONSULTATION', 'FOLLOW_UP', 'PROCEDURE', 'SCREENING', etc.
    specialty VARCHAR(100),

    -- Billing
    default_cpt_codes VARCHAR(10)[],

    color_code VARCHAR(7) DEFAULT '#3B82F6', -- For calendar display

    active BOOLEAN DEFAULT TRUE,

    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Appointments table (main)
CREATE TABLE appointments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    confirmation_number VARCHAR(20) UNIQUE NOT NULL,

    patient_id UUID NOT NULL REFERENCES patients(id),
    provider_id UUID NOT NULL REFERENCES providers(id),
    location_id UUID NOT NULL REFERENCES locations(id),
    appointment_type_id UUID NOT NULL REFERENCES appointment_types(id),

    -- Timing
    start_time TIMESTAMP WITH TIME ZONE NOT NULL,
    end_time TIMESTAMP WITH TIME ZONE NOT NULL,
    timezone VARCHAR(50) NOT NULL,

    -- Status
    status VARCHAR(20) NOT NULL DEFAULT 'BOOKED',
    -- Possible statuses: BOOKED, CONFIRMED, CHECKED_IN, IN_PROGRESS, COMPLETED, NO_SHOW, CANCELLED

    -- Booking information
    booking_source VARCHAR(50) NOT NULL, -- 'PATIENT_ONLINE', 'PATIENT_PHONE', 'STAFF', 'REFERRAL', etc.
    booked_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    booked_by UUID, -- Staff member who booked if applicable

    -- Patient confirmation
    patient_confirmed BOOLEAN DEFAULT FALSE,
    confirmed_at TIMESTAMP WITH TIME ZONE,

    -- Check-in
    checked_in_at TIMESTAMP WITH TIME ZONE,

    -- Cancellation
    cancelled_at TIMESTAMP WITH TIME ZONE,
    cancelled_by UUID, -- Patient or staff ID
    cancellation_reason VARCHAR(50),
    cancellation_notes TEXT,

    -- Clinical information
    chief_complaint TEXT,
    visit_reason TEXT,
    special_instructions TEXT,

    -- Reminders sent
    reminders_sent JSONB DEFAULT '[]'::jsonb,

    -- Waitlist
    filled_from_waitlist BOOLEAN DEFAULT FALSE,
    waitlist_entry_id UUID,

    -- Follow-up
    requires_follow_up BOOLEAN DEFAULT FALSE,
    follow_up_timeframe VARCHAR(50), -- '2_WEEKS', '1_MONTH', '3_MONTHS', etc.
    follow_up_appointment_id UUID REFERENCES appointments(id),

    -- HIPAA audit trail
    audit_log JSONB DEFAULT '[]'::jsonb,

    -- Metadata
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT valid_appointment_time CHECK (end_time > start_time)
);

CREATE INDEX idx_appointments_patient ON appointments(patient_id);
CREATE INDEX idx_appointments_provider ON appointments(provider_id);
CREATE INDEX idx_appointments_location ON appointments(location_id);
CREATE INDEX idx_appointments_start_time ON appointments(start_time);
CREATE INDEX idx_appointments_status ON appointments(status);
CREATE INDEX idx_appointments_confirmation ON appointments(confirmation_number);

-- Waitlist table
CREATE TABLE waitlist (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    patient_id UUID NOT NULL REFERENCES patients(id),

    -- Requested appointment details
    requested_providers UUID[] NOT NULL, -- Array of acceptable provider IDs
    requested_locations UUID[], -- Array of acceptable location IDs
    requested_appointment_types UUID[] NOT NULL,

    -- Timing preferences
    preferred_date_from DATE,
    preferred_date_until DATE,
    preferred_times_of_day VARCHAR(20)[], -- 'MORNING', 'AFTERNOON', 'EVENING'
    preferred_days_of_week INTEGER[], -- 0-6

    -- Priority
    clinical_priority VARCHAR(20) DEFAULT 'ROUTINE', -- 'URGENT', 'SEMI_URGENT', 'ROUTINE'
    urgency_score INTEGER DEFAULT 50, -- 0-100

    -- Contact preferences
    contact_methods VARCHAR(20)[] DEFAULT ARRAY['SMS', 'EMAIL'], -- How to notify when slot available

    -- Status
    status VARCHAR(20) DEFAULT 'ACTIVE', -- 'ACTIVE', 'MATCHED', 'EXPIRED', 'CANCELLED'

    -- Reason
    reason TEXT,

    -- Results
    matched_appointment_id UUID REFERENCES appointments(id),
    matched_at TIMESTAMP WITH TIME ZONE,

    -- Expiration
    expires_at TIMESTAMP WITH TIME ZONE,

    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    cancelled_at TIMESTAMP WITH TIME ZONE
);

CREATE INDEX idx_waitlist_patient ON waitlist(patient_id);
CREATE INDEX idx_waitlist_status ON waitlist(status) WHERE status = 'ACTIVE';
CREATE INDEX idx_waitlist_providers ON waitlist USING GIN(requested_providers);

-- Reminder log
CREATE TABLE reminder_log (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    appointment_id UUID NOT NULL REFERENCES appointments(id) ON DELETE CASCADE,

    channel VARCHAR(20) NOT NULL, -- 'SMS', 'EMAIL', 'PHONE', 'PUSH'
    timing VARCHAR(20) NOT NULL, -- '7_DAY', '48_HOUR', '24_HOUR', '2_HOUR'

    sent_at TIMESTAMP WITH TIME ZONE NOT NULL,
    status VARCHAR(20) NOT NULL, -- 'SENT', 'DELIVERED', 'FAILED', 'BOUNCED'

    -- External service IDs for tracking
    external_message_id VARCHAR(255),

    -- Content
    message_content TEXT,

    -- Delivery confirmation
    delivered_at TIMESTAMP WITH TIME ZONE,
    opened_at TIMESTAMP WITH TIME ZONE,
    clicked_at TIMESTAMP WITH TIME ZONE,

    -- Error tracking
    error_code VARCHAR(50),
    error_message TEXT,

    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_reminder_log_appointment ON reminder_log(appointment_id);
CREATE INDEX idx_reminder_log_status ON reminder_log(status);

This comprehensive database schema provides the foundation for a production-grade scheduling system. However, designing, implementing, and testing these schemas typically requires 2-3 months of development time. With JustCopy.ai, you can deploy production-ready database schemas instantly, with the platform’s AI agents automatically generating migrations, indexes, and seed data for your specific needs.

Part 2: Backend API Implementation

Core Scheduling Service

// Node.js + Express scheduling service
// File: services/scheduling-service.js

const { Pool } = require("pg");
const Redis = require("ioredis");
const { DateTime } = require("luxon");

class SchedulingService {
  constructor() {
    this.db = new Pool({
      connectionString: process.env.DATABASE_URL,
      ssl: { rejectUnauthorized: false },
    });

    this.redis = new Redis(process.env.REDIS_URL);
  }

  /**
   * Find available appointment slots for a provider
   */
  async findAvailableSlots(params) {
    const {
      providerId,
      locationId,
      appointmentTypeId,
      startDate,
      endDate,
      patientId,
    } = params;

    // Get appointment type details
    const appointmentType = await this.getAppointmentType(appointmentTypeId);

    // Get provider schedule
    const schedule = await this.getProviderSchedule(
      providerId,
      locationId,
      startDate,
      endDate
    );

    // Get existing appointments
    const existingAppointments = await this.getExistingAppointments(
      providerId,
      startDate,
      endDate
    );

    // Get schedule exceptions (PTO, holidays, etc.)
    const exceptions = await this.getScheduleExceptions(
      providerId,
      startDate,
      endDate
    );

    // Calculate available slots
    const slots = this.calculateAvailableSlots(
      schedule,
      existingAppointments,
      exceptions,
      appointmentType.default_duration_minutes
    );

    // Apply booking rules
    const filteredSlots = await this.applyBookingRules(
      slots,
      providerId,
      appointmentType,
      patientId
    );

    return filteredSlots;
  }

  async getProviderSchedule(providerId, locationId, startDate, endDate) {
    const query = `
            SELECT
                ps.*,
                l.timezone,
                l.hours_of_operation
            FROM provider_schedules ps
            JOIN locations l ON l.id = ps.location_id
            WHERE ps.provider_id = $1
                AND ps.location_id = $2
                AND ps.effective_from <= $4
                AND (ps.effective_until IS NULL OR ps.effective_until >= $3)
                AND ps.is_recurring = true
        `;

    const result = await this.db.query(query, [
      providerId,
      locationId,
      startDate,
      endDate,
    ]);

    return result.rows;
  }

  async getExistingAppointments(providerId, startDate, endDate) {
    const query = `
            SELECT
                id,
                start_time,
                end_time,
                status,
                appointment_type_id
            FROM appointments
            WHERE provider_id = $1
                AND start_time >= $2
                AND start_time < $3
                AND status IN ('BOOKED', 'CONFIRMED', 'CHECKED_IN', 'IN_PROGRESS')
            ORDER BY start_time
        `;

    const result = await this.db.query(query, [providerId, startDate, endDate]);

    return result.rows;
  }

  async getScheduleExceptions(providerId, startDate, endDate) {
    const query = `
            SELECT *
            FROM schedule_exceptions
            WHERE provider_id = $1
                AND start_datetime < $3
                AND end_datetime > $2
        `;

    const result = await this.db.query(query, [providerId, startDate, endDate]);

    return result.rows;
  }

  calculateAvailableSlots(
    schedule,
    existingAppointments,
    exceptions,
    durationMinutes
  ) {
    const slots = [];

    for (const scheduleBlock of schedule) {
      const dayOfWeek = scheduleBlock.day_of_week;

      // Generate slots for each occurrence of this day in the date range
      let currentDate = this.getNextDayOfWeek(
        new Date(scheduleBlock.effective_from),
        dayOfWeek
      );

      const endDate = scheduleBlock.effective_until
        ? new Date(scheduleBlock.effective_until)
        : new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); // 90 days

      while (currentDate <= endDate) {
        // Create DateTime objects for this day's schedule
        const scheduleStart = DateTime.fromObject({
          year: currentDate.getFullYear(),
          month: currentDate.getMonth() + 1,
          day: currentDate.getDate(),
          hour: parseInt(scheduleBlock.start_time.split(":")[0]),
          minute: parseInt(scheduleBlock.start_time.split(":")[1]),
          zone: scheduleBlock.timezone,
        });

        const scheduleEnd = DateTime.fromObject({
          year: currentDate.getFullYear(),
          month: currentDate.getMonth() + 1,
          day: currentDate.getDate(),
          hour: parseInt(scheduleBlock.end_time.split(":")[0]),
          minute: parseInt(scheduleBlock.end_time.split(":")[1]),
          zone: scheduleBlock.timezone,
        });

        // Generate slots for this day
        let slotStart = scheduleStart;

        while (slotStart.plus({ minutes: durationMinutes }) <= scheduleEnd) {
          const slotEnd = slotStart.plus({ minutes: durationMinutes });

          // Check if slot conflicts with existing appointment
          const hasConflict = existingAppointments.some((apt) => {
            const aptStart = DateTime.fromJSDate(new Date(apt.start_time));
            const aptEnd = DateTime.fromJSDate(new Date(apt.end_time));

            return (
              (slotStart >= aptStart && slotStart < aptEnd) ||
              (slotEnd > aptStart && slotEnd <= aptEnd) ||
              (slotStart <= aptStart && slotEnd >= aptEnd)
            );
          });

          // Check if slot conflicts with exception
          const hasException = exceptions.some((exc) => {
            const excStart = DateTime.fromJSDate(new Date(exc.start_datetime));
            const excEnd = DateTime.fromJSDate(new Date(exc.end_datetime));

            return (
              (slotStart >= excStart && slotStart < excEnd) ||
              (slotEnd > excStart && slotEnd <= excEnd) ||
              (slotStart <= excStart && slotEnd >= excEnd)
            );
          });

          if (!hasConflict && !hasException) {
            slots.push({
              start: slotStart.toJSDate(),
              end: slotEnd.toJSDate(),
              startISO: slotStart.toISO(),
              endISO: slotEnd.toISO(),
              timezone: scheduleBlock.timezone,
              duration: durationMinutes,
            });
          }

          // Move to next slot (with buffer)
          slotStart = slotEnd.plus({ minutes: 5 }); // 5-minute buffer
        }

        // Move to next week
        currentDate.setDate(currentDate.getDate() + 7);
      }
    }

    return slots;
  }

  async applyBookingRules(slots, providerId, appointmentType, patientId) {
    const provider = await this.getProvider(providerId);
    const patient = patientId ? await this.getPatient(patientId) : null;

    const now = DateTime.now();

    return slots.filter((slot) => {
      const slotTime = DateTime.fromJSDate(slot.start);

      // Minimum advance booking time
      const hoursInAdvance = slotTime.diff(now, "hours").hours;
      if (hoursInAdvance < provider.min_advance_booking_hours) {
        return false;
      }

      // Maximum advance booking time
      const daysInAdvance = slotTime.diff(now, "days").days;
      if (daysInAdvance > provider.max_advance_booking_days) {
        return false;
      }

      // New patient rules
      if (patient && patient.total_appointments === 0) {
        if (!appointmentType.new_patients_allowed) {
          return false;
        }
        if (!provider.accepts_new_patients) {
          return false;
        }
      }

      // Online booking allowed
      if (!appointmentType.online_booking_allowed) {
        return false;
      }

      return true;
    });
  }

  /**
   * Book an appointment
   */
  async bookAppointment(appointmentData) {
    const {
      patientId,
      providerId,
      locationId,
      appointmentTypeId,
      startTime,
      endTime,
      timezone,
      chiefComplaint,
      visitReason,
      bookingSource,
      bookedBy,
    } = appointmentData;

    // Begin transaction
    const client = await this.db.connect();

    try {
      await client.query("BEGIN");

      // Double-check slot availability (prevent race conditions)
      const slotAvailable = await this.verifySlotAvailability(
        providerId,
        startTime,
        endTime,
        client
      );

      if (!slotAvailable) {
        throw new Error("SLOT_NO_LONGER_AVAILABLE");
      }

      // Generate confirmation number
      const confirmationNumber = this.generateConfirmationNumber();

      // Create appointment
      const insertQuery = `
                INSERT INTO appointments (
                    confirmation_number,
                    patient_id,
                    provider_id,
                    location_id,
                    appointment_type_id,
                    start_time,
                    end_time,
                    timezone,
                    status,
                    booking_source,
                    booked_by,
                    chief_complaint,
                    visit_reason,
                    audit_log
                ) VALUES (
                    $1, $2, $3, $4, $5, $6, $7, $8, 'BOOKED', $9, $10, $11, $12,
                    jsonb_build_array(
                        jsonb_build_object(
                            'action', 'CREATED',
                            'timestamp', NOW(),
                            'performed_by', $2,
                            'details', jsonb_build_object(
                                'booking_source', $9
                            )
                        )
                    )
                )
                RETURNING *
            `;

      const result = await client.query(insertQuery, [
        confirmationNumber,
        patientId,
        providerId,
        locationId,
        appointmentTypeId,
        startTime,
        endTime,
        timezone,
        bookingSource,
        bookedBy || patientId,
        chiefComplaint,
        visitReason,
      ]);

      const appointment = result.rows[0];

      // Update patient statistics
      await client.query(
        `
                UPDATE patients
                SET total_appointments = total_appointments + 1,
                    last_appointment_at = $2,
                    updated_at = NOW()
                WHERE id = $1
            `,
        [patientId, startTime]
      );

      await client.query("COMMIT");

      // Schedule reminders (async, outside transaction)
      setImmediate(() => {
        this.scheduleReminders(appointment.id).catch(console.error);
      });

      // Send confirmation (async)
      setImmediate(() => {
        this.sendBookingConfirmation(appointment.id).catch(console.error);
      });

      // Clear relevant caches
      await this.clearAvailabilityCache(providerId, startTime);

      return appointment;
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  }

  async verifySlotAvailability(providerId, startTime, endTime, client = null) {
    const db = client || this.db;

    const query = `
            SELECT COUNT(*) as conflict_count
            FROM appointments
            WHERE provider_id = $1
                AND status IN ('BOOKED', 'CONFIRMED', 'CHECKED_IN', 'IN_PROGRESS')
                AND (
                    (start_time >= $2 AND start_time < $3) OR
                    (end_time > $2 AND end_time <= $3) OR
                    (start_time <= $2 AND end_time >= $3)
                )
        `;

    const result = await db.query(query, [providerId, startTime, endTime]);

    return parseInt(result.rows[0].conflict_count) === 0;
  }

  generateConfirmationNumber() {
    // Generate format: APT-YYYYMMDD-XXXXX
    const date = DateTime.now().toFormat("yyyyLLdd");
    const random = Math.floor(10000 + Math.random() * 90000);
    return `APT-${date}-${random}`;
  }

  async scheduleReminders(appointmentId) {
    // This will be implemented in the reminder service section
    // Schedule 7-day, 48-hour, 24-hour, and 2-hour reminders
  }

  async sendBookingConfirmation(appointmentId) {
    // This will be implemented in the notification section
  }

  async clearAvailabilityCache(providerId, date) {
    const dateKey = DateTime.fromJSDate(new Date(date)).toFormat("yyyy-LL-dd");
    const cacheKey = `availability:${providerId}:${dateKey}`;
    await this.redis.del(cacheKey);
  }

  getNextDayOfWeek(date, targetDay) {
    const currentDay = date.getDay();
    const daysUntilTarget = (targetDay - currentDay + 7) % 7;
    const result = new Date(date);
    result.setDate(date.getDate() + daysUntilTarget);
    return result;
  }

  async getProvider(providerId) {
    const query = "SELECT * FROM providers WHERE id = $1";
    const result = await this.db.query(query, [providerId]);
    return result.rows[0];
  }

  async getPatient(patientId) {
    const query = "SELECT * FROM patients WHERE id = $1";
    const result = await this.db.query(query, [patientId]);
    return result.rows[0];
  }

  async getAppointmentType(appointmentTypeId) {
    const query = "SELECT * FROM appointment_types WHERE id = $1";
    const result = await this.db.query(query, [appointmentTypeId]);
    return result.rows[0];
  }
}

module.exports = SchedulingService;

This backend scheduling service handles the core logic for finding available slots and booking appointments. Building, testing, and debugging this service typically requires 1-2 months of development time. With JustCopy.ai, you get production-ready scheduling services immediately, with the platform’s AI agents automatically customizing the logic for your specific business rules and operational workflows.

Part 3: Calendar Integration

Google Calendar, Outlook, and iCal Support

// Calendar integration service
// File: services/calendar-service.js

const { google } = require("googleapis");
const ical = require("ical-generator");
const { DateTime } = require("luxon");

class CalendarService {
  /**
   * Generate iCal file for email attachments
   */
  generateICalFile(appointment) {
    const calendar = ical({
      name: "Medical Appointment",
      timezone: appointment.timezone,
    });

    calendar.createEvent({
      start: DateTime.fromJSDate(new Date(appointment.start_time)),
      end: DateTime.fromJSDate(new Date(appointment.end_time)),
      summary: `Medical Appointment - ${appointment.provider_name}`,
      description: this.generateAppointmentDescription(appointment),
      location: appointment.location_address,
      organizer: {
        name: appointment.provider_name,
        email: appointment.provider_email,
      },
      attendees: [
        {
          name: appointment.patient_name,
          email: appointment.patient_email,
          rsvp: true,
        },
      ],
      alarms: [
        {
          type: "display",
          trigger: 60 * 24, // 24 hours before
        },
        {
          type: "display",
          trigger: 60 * 2, // 2 hours before
        },
      ],
      url: `${process.env.APP_URL}/appointments/${appointment.id}`,
      status: "CONFIRMED",
    });

    return calendar.toString();
  }

  /**
   * Generate Google Calendar link
   */
  generateGoogleCalendarLink(appointment) {
    const start = DateTime.fromJSDate(
      new Date(appointment.start_time)
    ).toFormat("yyyyLLdd'T'HHmmss");
    const end = DateTime.fromJSDate(new Date(appointment.end_time)).toFormat(
      "yyyyLLdd'T'HHmmss"
    );

    const params = new URLSearchParams({
      action: "TEMPLATE",
      text: `Medical Appointment - ${appointment.provider_name}`,
      dates: `${start}/${end}`,
      details: this.generateAppointmentDescription(appointment),
      location: appointment.location_address,
      ctz: appointment.timezone,
    });

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

  /**
   * Generate Outlook calendar link
   */
  generateOutlookCalendarLink(appointment) {
    const start = DateTime.fromJSDate(new Date(appointment.start_time)).toISO();
    const end = DateTime.fromJSDate(new Date(appointment.end_time)).toISO();

    const params = new URLSearchParams({
      path: "/calendar/action/compose",
      rru: "addevent",
      subject: `Medical Appointment - ${appointment.provider_name}`,
      startdt: start,
      enddt: end,
      body: this.generateAppointmentDescription(appointment),
      location: appointment.location_address,
    });

    return `https://outlook.live.com/calendar/0/deeplink/compose?${params.toString()}`;
  }

  generateAppointmentDescription(appointment) {
    return `
Appointment Details:

Provider: ${appointment.provider_name}
Date: ${DateTime.fromJSDate(new Date(appointment.start_time)).toFormat("DDDD")}
Time: ${DateTime.fromJSDate(new Date(appointment.start_time)).toFormat(
      "t ZZZZ"
    )}

Location:
${appointment.location_name}
${appointment.location_address}
Phone: ${appointment.location_phone}

Confirmation Number: ${appointment.confirmation_number}

Please arrive 15 minutes early for check-in.
Bring your insurance card and photo ID.

To reschedule or cancel:
${process.env.APP_URL}/appointments/${appointment.id}
        `.trim();
  }

  /**
   * Sync appointment to provider's Google Calendar
   */
  async syncToProviderGoogleCalendar(appointmentId) {
    const appointment = await this.getAppointmentDetails(appointmentId);
    const provider = await this.getProvider(appointment.provider_id);

    if (!provider.google_calendar_id || !provider.google_refresh_token) {
      console.log("Provider does not have Google Calendar connected");
      return null;
    }

    // Initialize Google Calendar API
    const oauth2Client = new google.auth.OAuth2(
      process.env.GOOGLE_CLIENT_ID,
      process.env.GOOGLE_CLIENT_SECRET,
      process.env.GOOGLE_REDIRECT_URI
    );

    oauth2Client.setCredentials({
      refresh_token: provider.google_refresh_token,
    });

    const calendar = google.calendar({ version: "v3", auth: oauth2Client });

    // Create event
    const event = {
      summary: `Patient: ${appointment.patient_name}`,
      description: `
Appointment Type: ${appointment.appointment_type_name}
Patient: ${appointment.patient_name}
Phone: ${appointment.patient_phone}
Chief Complaint: ${appointment.chief_complaint || "Not specified"}

Confirmation: ${appointment.confirmation_number}
            `.trim(),
      location: appointment.location_address,
      start: {
        dateTime: new Date(appointment.start_time).toISOString(),
        timeZone: appointment.timezone,
      },
      end: {
        dateTime: new Date(appointment.end_time).toISOString(),
        timeZone: appointment.timezone,
      },
      reminders: {
        useDefault: false,
        overrides: [{ method: "popup", minutes: 15 }],
      },
      colorId: "9", // Blue for appointments
      extendedProperties: {
        private: {
          appointmentId: appointmentId,
          system: "scheduling-platform",
        },
      },
    };

    try {
      const response = await calendar.events.insert({
        calendarId: provider.google_calendar_id,
        resource: event,
      });

      // Store the Google Calendar event ID for future updates/cancellations
      await this.saveCalendarEventMapping(
        appointmentId,
        "GOOGLE",
        response.data.id
      );

      return response.data;
    } catch (error) {
      console.error("Error syncing to Google Calendar:", error);
      throw error;
    }
  }
}

module.exports = CalendarService;

Implementing robust calendar integration across multiple platforms is complex and time-consuming. JustCopy.ai provides production-ready calendar integration code that its AI agents automatically customize for your specific calendar requirements, including bidirectional sync, conflict resolution, and error handling.

Part 4: Automated Reminders System

SMS and Email Reminder Implementation

// Reminder service with Twilio and SendGrid
// File: services/reminder-service.js

const twilio = require("twilio");
const sendgrid = require("@sendgrid/mail");
const { DateTime } = require("luxon");

class ReminderService {
  constructor() {
    this.twilioClient = twilio(
      process.env.TWILIO_ACCOUNT_SID,
      process.env.TWILIO_AUTH_TOKEN
    );

    sendgrid.setApiKey(process.env.SENDGRID_API_KEY);
  }

  /**
   * Schedule all reminders for an appointment
   */
  async scheduleAllReminders(appointmentId) {
    const appointment = await this.getAppointmentDetails(appointmentId);
    const patient = await this.getPatient(appointment.patient_id);

    // Define reminder schedule
    const reminderSchedule = [
      { timing: "7_DAY", days: 7, channels: ["EMAIL"] },
      { timing: "48_HOUR", hours: 48, channels: ["SMS", "EMAIL"] },
      { timing: "24_HOUR", hours: 24, channels: ["SMS"] },
      { timing: "2_HOUR", hours: 2, channels: ["SMS"] },
    ];

    for (const reminder of reminderSchedule) {
      const sendTime = DateTime.fromJSDate(
        new Date(appointment.start_time)
      ).minus({
        days: reminder.days || 0,
        hours: reminder.hours || 0,
      });

      // Only schedule if in the future
      if (sendTime > DateTime.now()) {
        await this.scheduleReminder({
          appointmentId: appointmentId,
          patientId: patient.id,
          timing: reminder.timing,
          channels: reminder.channels,
          sendAt: sendTime.toJSDate(),
        });
      }
    }
  }

  async scheduleReminder(reminderData) {
    // In production, use a job queue like Bull or AWS SQS
    // For this example, we'll use a simple database-based approach

    const query = `
            INSERT INTO scheduled_reminders (
                appointment_id,
                patient_id,
                timing,
                channels,
                send_at,
                status
            ) VALUES ($1, $2, $3, $4, $5, 'PENDING')
        `;

    await this.db.query(query, [
      reminderData.appointmentId,
      reminderData.patientId,
      reminderData.timing,
      reminderData.channels,
      reminderData.sendAt,
    ]);
  }

  /**
   * Send SMS reminder
   */
  async sendSMSReminder(appointment, timing) {
    const patient = await this.getPatient(appointment.patient_id);

    // Verify consent
    if (!patient.consent_to_sms) {
      console.log("Patient has not consented to SMS");
      return null;
    }

    const message = this.generateSMSMessage(appointment, timing);

    try {
      const result = await this.twilioClient.messages.create({
        to: patient.phone,
        from: process.env.TWILIO_PHONE_NUMBER,
        body: message,
        statusCallback: `${process.env.API_URL}/webhooks/sms-status`,
      });

      // Log reminder
      await this.logReminder({
        appointmentId: appointment.id,
        channel: "SMS",
        timing: timing,
        status: "SENT",
        externalMessageId: result.sid,
        messageContent: message,
      });

      return result;
    } catch (error) {
      // Log failed reminder
      await this.logReminder({
        appointmentId: appointment.id,
        channel: "SMS",
        timing: timing,
        status: "FAILED",
        errorCode: error.code,
        errorMessage: error.message,
      });

      throw error;
    }
  }

  generateSMSMessage(appointment, timing) {
    const appointmentTime = DateTime.fromJSDate(
      new Date(appointment.start_time)
    ).setZone(appointment.timezone);

    const confirmLink = `${process.env.APP_URL}/confirm/${appointment.confirmation_number}`;

    switch (timing) {
      case "7_DAY":
        return `Reminder: You have an appointment with ${
          appointment.provider_name
        } on ${appointmentTime.toFormat(
          "EEE, MMM d"
        )} at ${appointmentTime.toFormat("h:mm a")}. Confirm: ${confirmLink}`;

      case "48_HOUR":
        return `Upcoming appointment: ${appointmentTime.toFormat(
          "EEE, MMM d"
        )} at ${appointmentTime.toFormat("h:mm a")} with ${
          appointment.provider_name
        }. Reply C to confirm, R to reschedule. ${confirmLink}`;

      case "24_HOUR":
        return `Tomorrow at ${appointmentTime.toFormat(
          "h:mm a"
        )}: Appointment with ${appointment.provider_name} at ${
          appointment.location_name
        }. ${confirmLink}`;

      case "2_HOUR":
        return `Your appointment is today at ${appointmentTime.toFormat(
          "h:mm a"
        )}. ${appointment.location_name}, ${
          appointment.location_address
        }. See you soon!`;

      default:
        return `Appointment reminder: ${appointmentTime.toFormat(
          "EEE, MMM d"
        )} at ${appointmentTime.toFormat("h:mm a")}. ${confirmLink}`;
    }
  }

  /**
   * Send email reminder
   */
  async sendEmailReminder(appointment, timing) {
    const patient = await this.getPatient(appointment.patient_id);

    if (!patient.consent_to_email) {
      console.log("Patient has not consented to email");
      return null;
    }

    const emailContent = this.generateEmailContent(appointment, timing);

    // Generate calendar file
    const calendarService = new CalendarService();
    const icsContent = calendarService.generateICalFile(appointment);

    const msg = {
      to: patient.email,
      from: {
        email: process.env.SENDGRID_FROM_EMAIL,
        name: process.env.PRACTICE_NAME,
      },
      subject: emailContent.subject,
      html: emailContent.html,
      attachments: [
        {
          content: Buffer.from(icsContent).toString("base64"),
          filename: "appointment.ics",
          type: "text/calendar",
          disposition: "attachment",
        },
      ],
      trackingSettings: {
        clickTracking: { enable: true },
        openTracking: { enable: true },
      },
      customArgs: {
        appointmentId: appointment.id,
        timing: timing,
      },
    };

    try {
      const result = await sendgrid.send(msg);

      // Log reminder
      await this.logReminder({
        appointmentId: appointment.id,
        channel: "EMAIL",
        timing: timing,
        status: "SENT",
        externalMessageId: result[0].headers["x-message-id"],
        messageContent: emailContent.html,
      });

      return result;
    } catch (error) {
      // Log failed reminder
      await this.logReminder({
        appointmentId: appointment.id,
        channel: "EMAIL",
        timing: timing,
        status: "FAILED",
        errorCode: error.code,
        errorMessage: error.message,
      });

      throw error;
    }
  }

  generateEmailContent(appointment, timing) {
    const appointmentTime = DateTime.fromJSDate(
      new Date(appointment.start_time)
    ).setZone(appointment.timezone);

    const confirmLink = `${process.env.APP_URL}/confirm/${appointment.confirmation_number}`;
    const rescheduleLink = `${process.env.APP_URL}/reschedule/${appointment.confirmation_number}`;
    const cancelLink = `${process.env.APP_URL}/cancel/${appointment.confirmation_number}`;

    const calendarService = new CalendarService();
    const googleLink = calendarService.generateGoogleCalendarLink(appointment);
    const outlookLink =
      calendarService.generateOutlookCalendarLink(appointment);

    let subject, greeting;

    switch (timing) {
      case "7_DAY":
        subject = `Upcoming Appointment - ${appointmentTime.toFormat("MMM d")}`;
        greeting = "Your appointment is coming up next week.";
        break;
      case "48_HOUR":
        subject = `Appointment in 2 Days - ${appointmentTime.toFormat(
          "EEE, h:mm a"
        )}`;
        greeting = "This is a reminder that your appointment is in 2 days.";
        break;
      case "24_HOUR":
        subject = `Tomorrow: Appointment at ${appointmentTime.toFormat(
          "h:mm a"
        )}`;
        greeting = "Your appointment is tomorrow!";
        break;
      case "2_HOUR":
        subject = `Appointment Today at ${appointmentTime.toFormat("h:mm a")}`;
        greeting = "Your appointment is in 2 hours.";
        break;
    }

    const html = `
<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            font-family: Arial, sans-serif;
            line-height: 1.6;
            color: #333;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
        }
        .header {
            background-color: #0066cc;
            color: white;
            padding: 20px;
            text-align: center;
        }
        .content {
            background-color: #f9f9f9;
            padding: 30px;
            border-radius: 5px;
        }
        .appointment-details {
            background-color: white;
            padding: 20px;
            margin: 20px 0;
            border-left: 4px solid #0066cc;
        }
        .button {
            display: inline-block;
            padding: 12px 24px;
            margin: 10px 5px;
            background-color: #0066cc;
            color: white;
            text-decoration: none;
            border-radius: 5px;
        }
        .button-secondary {
            background-color: #6c757d;
        }
        .calendar-links {
            margin: 20px 0;
        }
        .calendar-links a {
            margin-right: 15px;
            color: #0066cc;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h2>Appointment Reminder</h2>
        </div>
        <div class="content">
            <p><strong>${greeting}</strong></p>

            <div class="appointment-details">
                <h3>Appointment Details</h3>
                <p><strong>Date:</strong> ${appointmentTime.toFormat(
                  "EEEE, MMMM d, yyyy"
                )}</p>
                <p><strong>Time:</strong> ${appointmentTime.toFormat(
                  "h:mm a ZZZZ"
                )}</p>
                <p><strong>Provider:</strong> ${appointment.provider_name}</p>
                <p><strong>Location:</strong><br>
                   ${appointment.location_name}<br>
                   ${appointment.location_address}</p>
                <p><strong>Phone:</strong> ${appointment.location_phone}</p>
                <p><strong>Confirmation #:</strong> ${
                  appointment.confirmation_number
                }</p>
            </div>

            <div style="text-align: center;">
                <a href="${confirmLink}" class="button">Confirm Appointment</a>
                <a href="${rescheduleLink}" class="button button-secondary">Reschedule</a>
            </div>

            <div class="calendar-links">
                <p><strong>Add to Calendar:</strong></p>
                <a href="${googleLink}">Google Calendar</a> |
                <a href="${outlookLink}">Outlook</a>
                <p><em>Or use the attached .ics file for other calendar applications.</em></p>
            </div>

            <div style="margin-top: 30px; padding: 15px; background-color: #fff3cd; border-radius: 5px;">
                <h4>Before Your Visit:</h4>
                <ul>
                    <li>Arrive 15 minutes early for check-in</li>
                    <li>Bring your insurance card and photo ID</li>
                    <li>Bring a list of current medications</li>
                    <li>Complete any pre-visit forms online (if available)</li>
                </ul>
            </div>

            <div style="text-align: center; margin-top: 20px;">
                <p><small>Need to cancel? <a href="${cancelLink}">Click here</a></small></p>
            </div>
        </div>
    </div>
</body>
</html>
        `;

    return { subject, html };
  }

  async logReminder(reminderData) {
    const query = `
            INSERT INTO reminder_log (
                appointment_id,
                channel,
                timing,
                sent_at,
                status,
                external_message_id,
                message_content,
                error_code,
                error_message
            ) VALUES ($1, $2, $3, NOW(), $4, $5, $6, $7, $8)
        `;

    await this.db.query(query, [
      reminderData.appointmentId,
      reminderData.channel,
      reminderData.timing,
      reminderData.status,
      reminderData.externalMessageId || null,
      reminderData.messageContent || null,
      reminderData.errorCode || null,
      reminderData.errorMessage || null,
    ]);
  }
}

module.exports = ReminderService;

Building a reliable, HIPAA-compliant reminder system with multi-channel support is complex. JustCopy.ai provides production-ready reminder services with built-in retry logic, delivery tracking, and compliance features, all customizable through its AI agents.

Due to length constraints, I’ll summarize the remaining sections. The complete implementation would continue with…

Part 5: Time Zone Handling

  • Luxon library for timezone conversions
  • Storing all times in UTC in database
  • Converting to patient/provider timezones for display
  • Handling daylight saving time transitions

Part 6: Waitlist Management

  • Automated matching algorithm
  • Multi-factor scoring (urgency, wait time, preferences)
  • Real-time notifications when slots open
  • Time-limited offers (30-minute claim window)

Part 7: HIPAA Compliance

  • Data encryption (at rest and in transit)
  • Access controls and audit logging
  • Business Associate Agreements
  • Secure API authentication (JWT with refresh tokens)
  • PHI handling best practices

Part 8: EHR Integration

  • FHIR API integration (Epic, Cerner)
  • Athenahealth REST API
  • Bidirectional sync
  • Error handling and reconciliation

Conclusion

Building a production-ready appointment scheduling system requires significant expertise across backend development, frontend engineering, database design, security, compliance, and healthcare workflows. The traditional approach requires:

Time: 6-12 months Cost: $150K-$500K Team: Backend developers, frontend developers, DevOps engineers, QA testers, security specialists

Alternative: Use JustCopy.ai

Instead of building from scratch, JustCopy.ai lets you copy a proven, production-ready appointment scheduling platform and customize it for your specific needs in 2-4 weeks. The platform’s 10 specialized AI agents handle:

  1. Code Generation Agent: Customizes the codebase for your workflows
  2. Testing Agent: Generates comprehensive test suites
  3. Security Agent: Implements HIPAA compliance and encryption
  4. Integration Agent: Connects to your EHR, calendar, and communication systems
  5. Database Agent: Optimizes schemas and queries
  6. API Agent: Creates RESTful APIs with proper authentication
  7. Frontend Agent: Generates responsive, accessible UI components
  8. DevOps Agent: Sets up CI/CD pipelines and deployment automation
  9. Monitoring Agent: Implements logging, alerting, and performance tracking
  10. Documentation Agent: Creates technical documentation and user guides

All code is production-ready, fully tested, HIPAA-compliant, and deployable to your infrastructure.

Start building your appointment scheduling system today with JustCopy.aiβ€”copy, customize, and deploy in weeks instead of months.

πŸš€

Build This with JustCopy.ai

Skip months of development with 10 specialized AI agents. JustCopy.ai can copy, customize, and deploy this application instantly. Our AI agents write code, run tests, handle deployment, and monitor your applicationβ€”all following healthcare industry best practices and HIPAA compliance standards.