Skip to main content

Best Boilerplates for Booking and Scheduling Apps 2026

·StarterPick Team
Share:

TL;DR

  • Cal.com is the correct default for most scheduling products — self-hosted MIT, enterprise-grade, and far more complete than anything you'd build in the same timeframe.
  • Build custom only when your booking logic is deeply product-specific: multi-resource allocation, dynamic pricing engines, group session management, or white-label requirements that Cal.com's paid tier doesn't cover.
  • Nylas and Cronofy are calendar sync APIs worth evaluating if your product needs bidirectional calendar integration across multiple providers without managing OAuth tokens and webhook payloads yourself.
  • Time zones are where booking apps fail — store everything in UTC, test with users in different DST-transition zones, and handle the edge case where a recurring appointment crosses a DST boundary.
  • Stripe payment at booking time requires careful decision-making around deposits vs full payment, refund policies, and no-show fee enforcement — these are product decisions that shape the architecture.

Key Takeaways

Scheduling software has a deceptively simple description — "let someone book time with you" — that hides substantial technical complexity. A mature booking system requires: per-provider availability windows with exception dates, buffer time between appointments, maximum daily booking limits, calendar sync in both directions (booking creates a calendar event; external calendar blocks create unavailability), time zone handling across providers and attendees, DST transitions for recurring appointments, payment collection with configurable refund policies, reminder sequences via email and SMS, cancellation and rescheduling flows with configurable lead time requirements, and no-show handling.

Each of these has edge cases. Availability calculation is an interval scheduling problem. Calendar sync is an eventually-consistent distributed data problem. Time zone handling has the classic DST pitfalls. Payment refunds for canceled appointments require policy decisions that your application must enforce programmatically.

The reason Cal.com exists — and why it's MIT licensed with self-hosting support — is that these problems are genuinely hard and have been solved. Unless your booking logic requires something fundamentally different from "a person books a slot with a provider," the engineering cost of rewriting this from scratch is not justified.


The Booking Product Complexity Spectrum

Understanding where your product sits on the complexity spectrum determines whether Cal.com is sufficient or whether custom logic is required.

Simple Booking

A single provider with fixed availability windows, a list of service types with set durations, and one-on-one appointment booking. No team routing, no multi-resource constraints, no dynamic pricing. This is Cal.com's sweet spot. Self-host Cal.com, customize the booking page, connect Stripe and Google Calendar, and you have a production booking system in an afternoon.

Hair salons, personal trainers, consultants taking discovery calls, therapists booking intake sessions — these all fit the simple booking pattern perfectly. The complexity is managed for you.

Medium Complexity (Multi-Provider Teams)

A team of providers where a client books with any available team member or a specific provider. Requires round-robin or weighted assignment logic. Cal.com handles this natively with its team scheduling and round-robin event types. Still a Cal.com use case, though you may need to customize the assignment logic beyond what Cal.com's UI exposes.

High Complexity (Multi-Resource, Dynamic Pricing)

Booking that requires allocating multiple resources simultaneously — a conference room, an AV system, and a facilitator for a training session; a boat, a captain, and a dive instructor for a diving tour; a treatment room and specific therapist and specific equipment for a spa service. This is multi-resource booking where availability is the intersection of multiple resource calendars.

Cal.com does not handle this natively. A custom booking engine is required, which means the full complexity of availability calculation, conflict detection, and resource constraint satisfaction falls on you.

Dynamic pricing — where price varies based on time of day, day of week, demand, or lead time — is also a custom case. Yoga studios charging more for weekend morning classes, salons with peak-hour pricing, venues with day-of-week rates. None of the major open source booking tools handle dynamic pricing natively.

Very High Complexity (Marketplace Booking)

Airbnb-style or Booking.com-style: many providers list availability, customers search and book, platform handles payments with split settlement to multiple parties. This is a marketplace with embedded booking, not a scheduling tool, and requires building from scratch on top of your own database and Stripe Connect integration.


Quick Comparison

ToolPriceCalendar SyncPaymentsWhite-labelBest For
Cal.comFree (self-host) / $15/moGoogle, Outlook, AppleStripe✅ (paid)Most scheduling needs
Calendly$10-$20/moGoogle, OutlookStripeSimple booking
Custom Next.jsDev costGoogle APIStripeIntegrated scheduling
NylasUsage-basedGoogle, Outlook, ExchangeCalendar sync API layer
CronofyUsage-basedGoogle, Outlook, Apple, ExchangeEnterprise calendar sync

Cal.com — Best Open Source Booking

Price: Free (MIT) | Creator: Cal.com team

Enterprise-grade scheduling platform. Available types: one-on-one, round-robin (teams), collective, dynamic. Calendar integrations: Google Calendar, Outlook, Apple Calendar, and CalDAV. Payments: Stripe, PayPal. Video: Zoom, Google Meet, Teams. Webhooks for all booking events.

# Self-host Cal.com
git clone https://github.com/calcom/cal.com
cd cal.com
cp .env.example .env
# Configure DATABASE_URL, NEXTAUTH_SECRET, GOOGLE_API_CREDENTIALS, etc.
yarn install && yarn build && yarn db:push

Choose if: You need a complete scheduling product without building from scratch.

Cal.com Self-Host Requirements

Self-hosting Cal.com requires more infrastructure than a typical Next.js app. The minimum production setup: Node.js 18+, PostgreSQL 13+, Redis (for session storage and job queues), and an email provider for transactional emails. The monorepo structure means the initial yarn install is slow; first build takes 5–10 minutes on a standard developer machine.

Resource requirements: Cal.com is a reasonably heavy Next.js application. A t3.medium (2 vCPU, 4GB RAM) handles development and small production deployments. Production deployments with more than 100 bookings per day benefit from t3.large. The database is a bigger scaling concern than the application server for most deployments — aggressive caching of availability slots is important at volume.

Cal.com Cloud vs Self-Host

Cal.com Cloud (their SaaS offering starting at $12/user/month) includes: managed hosting, automatic updates, white-labeling on higher tiers, priority support, and access to enterprise integrations. Self-hosting includes: full MIT license, no per-user fees at any scale, complete customization access, and data sovereignty.

The real cost of self-hosting is ongoing maintenance: security patches, database backups, SSL certificate renewal, dependency updates. For a small team building a product with booking as one feature, the ops overhead of self-hosting Cal.com is substantial. Cal.com Cloud makes economic sense when you want to focus entirely on your core product differentiation without managing scheduling infrastructure.

The break-even math: if your product has more than 20 users who each need their own Cal.com account, self-hosting costs less than Cloud at scale. If Cal.com is one feature inside a larger product and you're embedding their API, a Cal.com team plan covering your org is often simpler than self-hosting.

Cal.com Embed and API

Cal.com exposes a full REST API that covers all booking operations. The Atoms package (@calcom/atoms) provides embeddable React components for booking flows you can drop into your application:

import Cal, { getCalApi } from "@calcom/embed-react";

export function BookingButton() {
  return (
    <Cal
      calLink="your-username/15min"
      style={{ width: "100%", height: "100%", overflow: "scroll" }}
      config={{ layout: "month_view" }}
    />
  );
}

The embed approach is the fastest path if you need booking as one feature of a larger product. No infrastructure to run, no maintenance overhead, Cal.com handles scheduling infrastructure and you handle your product logic.


Building Custom Booking with Next.js

For scheduling tightly integrated into a SaaS product:

// Time slot availability calculation
function getAvailableSlots(
  date: Date,
  providerAvailability: Availability[],
  existingBookings: Booking[],
  duration: number  // minutes
): TimeSlot[] {
  const dayAvailability = providerAvailability.find(
    a => a.dayOfWeek === date.getDay()
  );
  if (!dayAvailability) return [];

  const slots: TimeSlot[] = [];
  let current = combineDateTime(date, dayAvailability.startTime);
  const end = combineDateTime(date, dayAvailability.endTime);

  while (addMinutes(current, duration) <= end) {
    const slotEnd = addMinutes(current, duration);

    // Check no existing bookings overlap
    const isAvailable = !existingBookings.some(booking =>
      booking.startTime < slotEnd && booking.endTime > current
    );

    if (isAvailable) {
      slots.push({ startTime: current, endTime: slotEnd, available: true });
    }

    current = addMinutes(current, duration);  // Advance by slot duration
  }

  return slots;
}

Time Zone Handling

The most common booking app bug: time zones. The rule: store all times in UTC, display in local time.

// Store booking as UTC
const booking = await db.booking.create({
  data: {
    startTimeUtc: toZonedTime(localTime, providerTimezone).toISOString(),
    providerTimezone: 'America/New_York',
    attendeeTimezone: attendeeTimezone,
  },
});

// Display to attendee in their timezone
const displayTime = format(
  toZonedTime(booking.startTimeUtc, booking.attendeeTimezone),
  'EEEE, MMMM d, yyyy h:mm a zzz'
);
// "Friday, March 8, 2026 2:00 PM EST"

The DST edge case that trips up most implementations: recurring appointments. If a client books a recurring weekly appointment at 2 PM EST, and a DST transition occurs mid-series, the appointment should remain at 2 PM in the provider's local time — not at a fixed UTC offset that shifts by an hour. This means recurring appointments must be stored as local time + timezone, recalculated to UTC for each occurrence, not stored as a recurring UTC timestamp.

Common mistakes to avoid: using JavaScript's Date.toLocaleDateString() without specifying a timezone (it uses the server's timezone, not the user's), storing timezone offset (-05:00) instead of IANA timezone name (America/New_York) — offsets change with DST, IANA names don't — and displaying UTC times directly to users without conversion.


Google Calendar Integration

import { google } from 'googleapis';

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

// Create calendar event on booking confirmation
await calendar.events.insert({
  calendarId: 'primary',
  requestBody: {
    summary: `Meeting with ${attendee.name}`,
    description: meeting.notes,
    start: { dateTime: booking.startTimeUtc, timeZone: 'UTC' },
    end: { dateTime: booking.endTimeUtc, timeZone: 'UTC' },
    attendees: [{ email: attendee.email }, { email: provider.email }],
    conferenceData: {
      createRequest: { requestId: booking.id },  // Auto-create Google Meet
    },
  },
  conferenceDataVersion: 1,
});

Calendar Sync APIs: Nylas and Cronofy

If your product needs bidirectional calendar sync across Gmail, Outlook, Apple Calendar, and Exchange without managing the OAuth flow and webhook payloads for each provider separately, Nylas and Cronofy provide a unified API layer.

Nylas provides a single REST API for reading and writing calendar events, reading email (for CRM use cases), and managing contacts across all major providers. The developer experience is notably better than integrating Google Calendar API and Microsoft Graph API separately — one authentication flow, one event schema, one webhook endpoint for all calendar changes. Nylas is particularly useful if your application also needs email integration alongside scheduling.

Cronofy focuses specifically on calendar sync with an emphasis on enterprise and B2B use cases. It supports Exchange/EWS (used in corporate environments) more reliably than Nylas, making it a better choice if your customers are enterprise organizations with Exchange-based email. Cronofy also provides a Real-Time Scheduling API with built-in conflict detection across team members — useful for group availability use cases without building the intersection logic yourself.

Both are usage-based pricing with free development tiers. The cost at scale can become significant for consumer products with millions of users; it's more reasonable for B2B products with hundreds or thousands of users who need enterprise calendar integration.

Vyte is a team scheduling tool (not an API) worth knowing as a reference for group scheduling features — polling-based meeting scheduling where participants vote on time slots. If your product needs this pattern, understanding Vyte's UX gives you a model to work from.


Stripe Payments at Booking

Payment at booking time has more nuance than a standard e-commerce checkout, and the product decisions you make here shape the architecture significantly.

Deposit vs Full Payment

Service businesses often prefer collecting a deposit (typically 20–50% of the service price) at booking time to secure the appointment, with the balance collected at the appointment. This requires a split-payment flow: Stripe PaymentIntent for the deposit at booking, with the remaining amount captured or charged separately at service delivery.

Stripe's approach: create a PaymentIntent for the deposit amount only, and store the remaining balance in your database. When the appointment is completed, create a new PaymentIntent or use a saved payment method to charge the remainder.

// Create a payment intent for deposit only
const paymentIntent = await stripe.paymentIntents.create({
  amount: depositAmountCents,
  currency: 'usd',
  metadata: {
    bookingId: booking.id,
    type: 'deposit',
    remainingAmount: (totalPrice - depositAmount) * 100,
  },
  automatic_payment_methods: { enabled: true },
});

Cancellation Refund Policies

Cancellation refund policies need to be implemented as rules, not manual processes. A common policy: full refund if canceled 48+ hours before the appointment, 50% refund within 24–48 hours, no refund within 24 hours. This logic runs when a cancellation is processed:

function calculateRefundAmount(
  booking: Booking,
  cancelledAt: Date,
  policy: RefundPolicy
): number {
  const hoursUntilAppointment =
    differenceInHours(booking.startTime, cancelledAt);

  if (hoursUntilAppointment >= policy.fullRefundHours) {
    return booking.paidAmount;
  } else if (hoursUntilAppointment >= policy.partialRefundHours) {
    return Math.floor(booking.paidAmount * policy.partialRefundPercent);
  }
  return 0;
}

No-Show Fees

No-show fee enforcement requires a saved payment method on file for future charging. This is more complex than charging at booking: you need to store a Stripe Customer ID with a saved payment method, present the fee policy clearly at booking time, and have a workflow for the provider to mark an appointment as a no-show that triggers the charge.

The UX consideration: charging a no-show fee is a sensitive interaction. Build in a grace period (30–60 minutes post-appointment), require the provider to explicitly confirm the no-show before charging, and send the customer a notification before processing the charge. This reduces chargebacks and disputes significantly.


Booking Confirmation and Reminder Flow

The confirmation and reminder flow is often an afterthought in booking app development but has direct impact on appointment show rates.

Immediately on booking confirmation: send an HTML email with the appointment details, a calendar invite attachment (ICS file), provider contact information, location or video link, and any pre-appointment instructions. Generate the ICS attachment server-side using the ics npm package.

Reminder sequence for a 1-hour appointment:

  • 24 hours before: "Your appointment is tomorrow" reminder with reschedule/cancel links
  • 2 hours before: Final reminder with how-to-join details for video calls, or directions for in-person appointments
// Reminder job — run with a cron scheduler
export async function sendUpcomingReminders() {
  const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
  const windowEnd = new Date(tomorrow.getTime() + 60 * 60 * 1000); // ±1 hour

  const upcomingBookings = await prisma.booking.findMany({
    where: {
      startTimeUtc: { gte: tomorrow, lte: windowEnd },
      status: 'confirmed',
      reminderSentAt: null,
    },
    include: { attendee: true, provider: true },
  });

  for (const booking of upcomingBookings) {
    await sendReminderEmail({
      to: booking.attendee.email,
      bookingId: booking.id,
      startTime: booking.startTimeUtc,
      attendeeTimezone: booking.attendeeTimezone,
    });

    await prisma.booking.update({
      where: { id: booking.id },
      data: { reminderSentAt: new Date() },
    });
  }
}

Implement reminders as delayed background jobs created at booking time. BullMQ or Inngest work well: create a delayed job scheduled for 24 hours before the appointment when the booking is confirmed, and cancel the job if the appointment is rescheduled or canceled. This is more reliable than a polling cron job and eliminates the ±1 hour window imprecision.

Rescheduling must verify the new time slot is available (not just check the provider's calendar — also check for other bookings), cancel the original calendar event, create a new one, and re-send confirmation. The reminder jobs for the original time must also be canceled and re-queued for the new time.


Multi-Resource Booking

Multi-resource booking allocates multiple independent resources simultaneously. The availability constraint is the intersection of all required resource availabilities.

A training room booking example: availability = rooms with capacity at or above attendee count AND facilitator is available AND AV equipment is available. Finding valid slots requires querying availability for all three resource types, then finding time windows where all three are simultaneously free. This is an interval intersection problem that requires careful query design — naive N+1 queries per resource will be too slow at any real volume.

The database pattern: each resource has its own availability windows and booking records. The slot generation logic finds time windows where no resource in the required set has a conflicting booking. For small numbers of resources (2–5), this can be handled in application code. For larger systems, a dedicated interval scheduling query using PostgreSQL's range types (tstzrange) performs the intersection efficiently at the database layer.


Recurring Appointments

Subscription-based recurring appointments — weekly therapy sessions, biweekly coaching calls, monthly advisory meetings — require generating future appointment records while handling edge cases: provider vacation blocking specific instances, rescheduling one instance without affecting the series, and canceling the series vs canceling one instance.

The implementation decision: generate all future instances upfront at booking (simple query model but creates many records and requires cleanup if the series is modified), or generate on-demand (complex but handles changes cleanly with a single series record). Most production implementations use a hybrid: generate 4–8 weeks of instances immediately, and generate additional instances on a scheduled job as the horizon approaches.

The Outlook/Google Calendar model of storing a recurrence rule (RRULE) and expanding it on read is elegant but hard to implement correctly — especially when individual instances are modified, creating "exceptions" to the recurring pattern. Unless your team has deep calendar protocol expertise, the explicit instance-per-row model is more maintainable even if it produces more records.

For the broadest perspective on open source boilerplate options that can serve as the underlying infrastructure for booking features, see our guide to open source SaaS boilerplates. If authentication architecture is a decision you're working through for your booking product, see authentication setup in Next.js boilerplates. And the StarterPick boilerplate directory has side-by-side comparisons of the most popular options.

See also: best SaaS boilerplates 2026 for a full comparison of full-stack starters that pair well with a booking layer. For teams building multi-tenant scheduling platforms, best boilerplates for multi-tenant SaaS covers the right foundations.


Methodology

This article is based on hands-on evaluation of Cal.com, Nylas, and Cronofy APIs, as well as direct experience building custom booking systems on Next.js. Complexity spectrum classifications reflect common patterns in production booking applications across service industries. Stripe integration patterns are based on Stripe's documentation and recommended practices for service businesses. Cal.com feature coverage reflects the open source repository as of early 2026. No-show reduction figures reflect published research on appointment reminder effectiveness across healthcare, beauty, and professional services industries.

Check out this boilerplate

View Cal.comon StarterPick →

The SaaS Boilerplate Matrix (Free PDF)

20+ SaaS starters compared: pricing, tech stack, auth, payments, and what you actually ship with. Updated monthly. Used by 150+ founders.

Join 150+ SaaS founders. Unsubscribe in one click.