Best Boilerplates for Appointment Scheduling SaaS 2026
Scheduling SaaS Is a Proven Niche
Appointment scheduling software — Calendly, Cal.com, Acuity — is a large, proven market. Every professional service business needs booking software: coaches, consultants, doctors, lawyers, tutors, barbers, personal trainers.
Building a scheduling SaaS product means:
- Availability management — providers set their available hours
- Calendar integration — sync with Google Calendar, Outlook, Apple Calendar
- Booking flows — customers pick a time, receive confirmation
- Buffer time — automatic gaps between appointments
- Timezone handling — every interaction must be timezone-aware
- Payment at booking — Stripe integration for paid consultations
- Reminders — email and SMS before appointments
The hardest parts: timezone handling, calendar sync, and availability conflict detection.
TL;DR
Best starting points for appointment scheduling SaaS in 2026:
- Cal.com (open source, fork) — The most complete open-source scheduling system. MIT licensed, fork and white-label.
- Calcom atoms + Next.js — Embed Cal.com's scheduling UI as React components in your own SaaS.
- Nylas API + any SaaS boilerplate — Managed calendar integration (Google, Outlook, iCal).
- Google Calendar API + OpenSaaS — DIY scheduling with Google Calendar sync.
- Custom: Next.js + Prisma + date-fns-tz — Full control, timezone-safe implementation.
Key Takeaways
- Cal.com is MIT licensed — you can fork it, white-label it, and sell it as your own product
- Cal.com Atoms (their embeddable React components) let you add scheduling UI to any app
- Nylas handles Google Calendar, Outlook, and Apple Calendar sync under one API
- All scheduling logic must use UTC storage with timezone-aware display — never store local times
- Buffer time prevents back-to-back bookings — implement as a constraint in availability checking
- The average custom scheduling implementation takes 3-4 weeks; Cal.com fork is 1-2 weeks
Option 1: Fork Cal.com
Cal.com is the open-source Calendly alternative. Its stack: Next.js 15, Prisma, tRPC, Turborepo. MIT licensed.
git clone https://github.com/calcom/cal.com.git my-scheduler
cd my-scheduler
# Install dependencies:
yarn install
# Setup database:
cp .env.example .env
# Fill in: DATABASE_URL, NEXTAUTH_SECRET, GOOGLE_CLIENT_ID, etc.
yarn workspace @calcom/prisma db:push
yarn dev
Cal.com includes out of the box:
- Availability management with recurring schedules
- Google Calendar, Outlook, Apple Calendar bidirectional sync
- Booking page with timezone detection
- Email confirmations and reminders
- Stripe payment integration at booking
- Team scheduling (round robin, collective)
- Event types (one-on-one, group, recurring)
- Webhooks for booking events
- API for programmatic booking
White-labeling: Replace Cal.com branding in packages/ui and deploy under your domain.
Customization effort: 2-4 weeks to white-label and customize for a specific vertical.
Option 2: Cal.com Atoms (Embed Scheduling in Your SaaS)
Cal.com Atoms are React components that embed scheduling UI into your application:
npm install @calcom/atoms
// Embed the scheduler in your Next.js app:
import { CalProvider, Booker } from '@calcom/atoms';
export function SchedulingPage({ userId }: { userId: string }) {
return (
<CalProvider
clientId={process.env.NEXT_PUBLIC_CAL_CLIENT_ID!}
options={{
apiUrl: 'https://api.cal.com/v2',
refreshUrl: '/api/cal/refresh',
}}
>
<Booker
username="your-cal-username"
eventSlug="30min"
onCreateBooking={(booking) => {
console.log('Booking created:', booking);
// Update your database, send custom email, etc.
}}
/>
</CalProvider>
);
}
This approach: use any SaaS boilerplate (ShipFast, OpenSaaS) for auth/billing, embed Cal.com Atoms for the scheduling UI.
Trade-off: You are tied to Cal.com's infrastructure. Works well for adding scheduling to an existing SaaS; less appropriate for a scheduling-first product.
Option 3: Nylas API (Calendar Integration Layer)
Nylas provides a unified API for Google Calendar, Microsoft Outlook, and Apple iCloud:
npm install nylas
import Nylas from 'nylas';
const nylas = new Nylas({ apiKey: process.env.NYLAS_API_KEY! });
// Get user's availability:
export async function getAvailability(
nylasGrantId: string, // User's connected calendar grant
startTime: Date,
endTime: Date,
durationMinutes: number
) {
const availability = await nylas.calendars.getFreeBusy({
identifier: nylasGrantId,
requestBody: {
start_time: Math.floor(startTime.getTime() / 1000),
end_time: Math.floor(endTime.getTime() / 1000),
emails: [userEmail],
},
});
return availability;
}
// Create a calendar event when booking is confirmed:
export async function createBookingEvent(
nylasGrantId: string,
booking: {
title: string;
startTime: Date;
endTime: Date;
attendeeEmail: string;
meetingUrl: string;
}
) {
const event = await nylas.events.create({
identifier: nylasGrantId,
requestBody: {
title: booking.title,
when: {
start_time: Math.floor(booking.startTime.getTime() / 1000),
end_time: Math.floor(booking.endTime.getTime() / 1000),
},
participants: [
{ email: booking.attendeeEmail, status: 'noreply' },
],
conferencing: { details: { url: booking.meetingUrl } },
},
queryParams: { calendarId: 'primary', notify_participants: true },
});
return event;
}
Nylas pricing starts at $0 for development; $25/mo for production. It removes the calendar integration complexity entirely.
Building Custom: Availability Algorithm
If building from scratch, the availability algorithm:
// lib/availability.ts
import { addMinutes, isWithinInterval, startOfDay, endOfDay } from 'date-fns';
import { fromZonedTime, toZonedTime, format } from 'date-fns-tz';
interface WorkingHours {
dayOfWeek: number; // 0 = Sunday, 6 = Saturday
startTime: string; // "09:00"
endTime: string; // "17:00"
timezone: string; // "America/New_York"
}
interface ExistingBooking {
startTime: Date;
endTime: Date;
}
export function getAvailableSlots(
workingHours: WorkingHours[],
existingBookings: ExistingBooking[],
date: Date,
slotDurationMinutes: number,
bufferMinutes: number,
viewerTimezone: string
): Date[] {
const dayOfWeek = date.getDay();
const scheduleForDay = workingHours.find((wh) => wh.dayOfWeek === dayOfWeek);
if (!scheduleForDay) return [];
// Convert working hours to UTC:
const [startHour, startMin] = scheduleForDay.startTime.split(':').map(Number);
const [endHour, endMin] = scheduleForDay.endTime.split(':').map(Number);
const localDate = toZonedTime(date, scheduleForDay.timezone);
const workStart = fromZonedTime(
new Date(localDate.getFullYear(), localDate.getMonth(), localDate.getDate(), startHour, startMin),
scheduleForDay.timezone
);
const workEnd = fromZonedTime(
new Date(localDate.getFullYear(), localDate.getMonth(), localDate.getDate(), endHour, endMin),
scheduleForDay.timezone
);
const slots: Date[] = [];
let currentSlot = workStart;
while (addMinutes(currentSlot, slotDurationMinutes) <= workEnd) {
const slotEnd = addMinutes(currentSlot, slotDurationMinutes);
const slotWithBuffer = addMinutes(currentSlot, slotDurationMinutes + bufferMinutes);
// Check if slot overlaps with any existing booking:
const isBooked = existingBookings.some((booking) =>
isWithinInterval(currentSlot, {
start: addMinutes(booking.startTime, -bufferMinutes),
end: booking.endTime,
}) ||
isWithinInterval(slotEnd, {
start: booking.startTime,
end: addMinutes(booking.endTime, bufferMinutes),
})
);
if (!isBooked) {
slots.push(currentSlot);
}
currentSlot = addMinutes(currentSlot, slotDurationMinutes + bufferMinutes);
}
return slots;
}
Timezone-Safe Storage
The critical rule: always store times in UTC, display in user timezone:
// API route: create booking
export async function POST(req: Request) {
const { startTimeUTC, durationMinutes, attendeeTimezone } = await req.json();
const startTime = new Date(startTimeUTC); // UTC from client
const endTime = addMinutes(startTime, durationMinutes);
const booking = await db.booking.create({
data: {
startTime, // UTC in database
endTime, // UTC in database
attendeeTimezone, // Store for display purposes
},
});
// Send confirmation email with timezone-correct time:
const displayTime = format(
toZonedTime(startTime, attendeeTimezone),
"EEEE, MMMM d 'at' h:mm a zzz",
{ timeZone: attendeeTimezone }
);
await sendConfirmationEmail({ time: displayTime });
}
Payment at Booking (Stripe)
// Create payment intent when booking is initiated:
export async function initiateBooking(
serviceId: string,
slotTime: Date,
attendeeEmail: string
) {
const service = await db.service.findUnique({ where: { id: serviceId } });
if (service.price === 0) {
// Free booking — skip payment:
return createConfirmedBooking(serviceId, slotTime, attendeeEmail);
}
// Paid booking — create payment intent:
const paymentIntent = await stripe.paymentIntents.create({
amount: service.price,
currency: 'usd',
metadata: { serviceId, slotTime: slotTime.toISOString(), attendeeEmail },
});
// Create pending booking (confirmed on payment success):
const booking = await db.booking.create({
data: {
serviceId,
startTime: slotTime,
attendeeEmail,
status: 'PENDING_PAYMENT',
stripePaymentIntentId: paymentIntent.id,
},
});
return { clientSecret: paymentIntent.client_secret, bookingId: booking.id };
}
Recommended Starting Point by Product Type
| Scheduling Product | Starting Point |
|---|---|
| Calendly clone | Fork Cal.com |
| Scheduling feature in SaaS | Cal.com Atoms |
| Multi-calendar sync | Nylas API + ShipFast |
| Niche vertical scheduler | OpenSaaS + custom availability |
| Healthcare scheduling | Custom (HIPAA compliance required) |
Methodology
Based on publicly available information from Cal.com documentation, Nylas API documentation, and scheduling builder community resources as of March 2026.
Building a scheduling SaaS? StarterPick helps you find the right SaaS boilerplate to build your booking product on top of.