Skip to main content
RapidDev - Software Development Agency

How to Build a Scheduling App with Lovable

Build a Calendly-style scheduling app in Lovable where hosts define availability windows and guests self-book appointments. All times are stored as UTC in Supabase, timezone conversion happens at render time, and a unique database constraint prevents double-bookings at the database level. The guest-facing page requires no login. Build time is about 2 hours.

What you'll build

  • Host profile with a shareable public URL containing their unique booking slug
  • Availability slots table storing recurring weekly windows in the host's local timezone
  • Guest-facing booking page with a Calendar date picker and time slot list
  • UTC storage for all appointment timestamps with timezone display conversion
  • Double-booking prevention via a unique constraint on appointments(host_id, starts_at)
  • Host dashboard showing upcoming appointments with guest names and cancellation controls
  • Appointment confirmation screen with a copyable reference number
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read2 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a Calendly-style scheduling app in Lovable where hosts define availability windows and guests self-book appointments. All times are stored as UTC in Supabase, timezone conversion happens at render time, and a unique database constraint prevents double-bookings at the database level. The guest-facing page requires no login. Build time is about 2 hours.

What you're building

The scheduling app has two sides. The host side (requires auth) is where someone sets up their meeting type: a name, duration, description, and a weekly availability schedule. Availability is stored as rows in an availability_slots table — each row represents a recurring window on a specific day of the week (e.g. Tuesday 10:00–12:00). These are stored as plain time values in the host's chosen timezone.

The guest side is a public page at /schedule/:slug. The guest picks a date on the Calendar component. The app then generates the list of valid time slots for that date by reading the host's availability for that weekday and subtracting any existing appointments on that day. Slots that are already booked appear disabled or are simply not shown.

All appointment timestamps are converted to UTC before being saved to Supabase. The host dashboard displays times in the host's timezone. The guest confirmation shows times in the guest's browser timezone. The unique constraint on appointments(host_id, starts_at) is the final safety net — if a race condition occurs and two guests try to book the same slot simultaneously, the database rejects the second insert.

Final result

A complete scheduling app with public booking pages, timezone-correct time display, and database-enforced double-booking prevention.

Tech stack

LovableFrontend app builder
SupabaseDatabase and Auth
shadcn/uiCalendar, Card, Badge, Dialog, Separator
date-fnsDate arithmetic, formatting, timezone-aware parsing
react-hook-form + zodGuest booking form validation

Prerequisites

  • Lovable Pro account for multi-file generation
  • Supabase project with Auth enabled for host accounts
  • Supabase credentials saved to Cloud tab → Secrets
  • Basic familiarity with IANA timezone names (like 'America/New_York')
  • Two browser profiles or incognito windows for testing the guest and host sides

Build steps

1

Create the scheduling schema with the double-booking constraint

Prompt Lovable to create the database schema. The critical piece is the unique constraint on appointments that prevents double-bookings at the database level regardless of what happens in the application logic.

prompt.txt
1Create a Supabase schema for a scheduling app.
2
3Tables:
4- hosts: id (uuid pk, references auth.users), name (text), bio (text), slug (text unique), timezone (text, IANA e.g. 'America/Chicago'), avatar_url (text), created_at
5- meeting_types: id (uuid pk), host_id (references hosts), name (text), duration_minutes (int default 30), description (text), is_active (bool default true), created_at
6- availability_slots: id (uuid pk), host_id (references hosts), day_of_week (int, 0=Sun to 6=Sat), start_time (time), end_time (time), is_active (bool default true)
7- appointments: id (uuid pk), host_id (references hosts), meeting_type_id (references meeting_types), guest_name (text not null), guest_email (text not null), starts_at (timestamptz not null), ends_at (timestamptz not null), guest_notes (text), status (text default 'confirmed' check: confirmed|cancelled), created_at
8
9Critical constraint:
10ALTER TABLE appointments ADD CONSTRAINT no_double_booking UNIQUE (host_id, starts_at) DEFERRABLE INITIALLY IMMEDIATE;
11
12Note: filter WHERE status != 'cancelled' via a partial unique index instead:
13CREATE UNIQUE INDEX appointments_no_double_booking ON appointments (host_id, starts_at) WHERE status = 'confirmed';
14
15RLS:
16- hosts: SELECT public; INSERT/UPDATE where id = auth.uid()
17- meeting_types: SELECT public; INSERT/UPDATE/DELETE where host_id = auth.uid()
18- availability_slots: SELECT public; INSERT/UPDATE/DELETE where host_id = auth.uid()
19- appointments: SELECT where host_id = auth.uid(); INSERT public (any guest can book)
20
21Create an index: CREATE INDEX idx_appointments_host_date ON appointments (host_id, starts_at);

Pro tip: Use a partial unique index (WHERE status = 'confirmed') rather than a full unique constraint. This way a cancelled appointment slot can be rebooked — you are only preventing two confirmed appointments at the same time, not all historical records.

Expected result: All tables are created. The partial unique index exists on appointments. RLS is enabled on all tables. The public SELECT on hosts, meeting_types, and availability_slots allows the guest booking page to work without auth.

2

Build the host onboarding and meeting type setup

Ask Lovable to build the host setup flow. A new host fills in their profile, gets a slug, and creates at least one meeting type before sharing their link.

prompt.txt
1Build a host setup flow at src/pages/HostSetup.tsx (requires auth).
2
3Step 1 Profile:
4- Form fields: name, bio (Textarea), slug (Input, validate URL-safe with regex /^[a-z0-9-]+$/), timezone (Select with 15 common IANA options), avatar upload to Supabase Storage
5- Upsert into hosts table on save
6- Show preview URL: /schedule/{slug}
7
8Step 2 Meeting type:
9- Form fields: name (e.g. '30-minute intro call'), duration_minutes (Select: 15/30/45/60), description (Textarea)
10- Insert into meeting_types table
11- Show success and 'Share your link' button
12
13Step 3 Availability:
14- Render 7 rows (Sunday through Saturday)
15- Each row: day name, Switch (enabled/disabled), start time Select (30-min intervals), end time Select
16- When Switch is disabled, hide the time selectors
17- On save, delete existing availability_slots for this host and re-insert current configuration
18- Show 'Your schedule is live' confirmation
19
20Also build a combined settings page at src/pages/HostSettings.tsx that shows all three sections on one page for returning hosts updating their setup.

Pro tip: When saving availability, delete-then-reinsert is simpler and safer than upsert because the user may have removed a day. Ask Lovable to wrap the delete and insert in a Supabase transaction: supabase.rpc('update_availability', { slots: [...] }) where the RPC function handles the delete and insert atomically.

Expected result: A new host can complete all three setup steps. Their availability is saved. Visiting /schedule/their-slug shows the public booking page.

3

Build the guest-facing scheduling page

Ask Lovable to create the public scheduling page. The guest picks a date, picks a time, fills in their details, and gets a confirmation. All times are shown in the guest's detected browser timezone.

prompt.txt
1Build a public scheduling page at src/pages/SchedulePage.tsx with route /schedule/:slug.
2
3On load:
4- Fetch host by slug (name, bio, avatar_url, timezone)
5- Fetch meeting types for this host (show Select if multiple, auto-select if one)
6- Fetch availability_slots for this host
7- Detect guest timezone: const guestTz = Intl.DateTimeFormat().resolvedOptions().timeZone
8
9Date selection:
10- Render shadcn/ui Calendar
11- Disable past dates
12- Disable dates where no availability_slot exists for that day_of_week
13- On date select: compute available time slots
14
15Slot computation (client-side for this simpler version):
161. Find availability_slots row matching selected date's day_of_week
172. Generate slot start times from start_time to end_time with duration_minutes increments
183. Fetch appointments WHERE host_id = ? AND starts_at::date = selectedDate AND status = 'confirmed'
194. Remove slots that overlap any existing appointment
205. Convert remaining UTC slot times to guestTz for display
21
22Time slot display:
23- Show slots as clickable Cards or a vertical list of Buttons
24- Each shows the time in guest timezone (e.g. '2:30 PM')
25- Below the list show: 'Times shown in [guestTz]'
26
27Booking form (shown after slot selected):
28- guest_name (Input, required)
29- guest_email (Input, email, required)
30- guest_notes (Textarea, optional, 'Anything I should know?')
31- Submit inserts into appointments. On unique constraint error (409/23505), show 'That slot was just booked. Please choose another.'
32
33Confirmation state:
34- Show appointment details
35- 'Add to calendar' link (.ics download)
36- Reference number (first 8 chars of appointment UUID, uppercase)

Expected result: Visiting /schedule/:slug shows the host profile and Calendar. Selecting a date shows available slots. Completing the form creates the appointment and shows the confirmation. Trying to book an already-taken slot shows an error.

4

Build the host dashboard

Ask Lovable to build the host's private dashboard showing upcoming and past appointments. Hosts can cancel appointments and see guest contact details.

prompt.txt
1Build a host appointments dashboard at src/pages/HostDashboard.tsx (requires auth).
2
3Requirements:
4- Fetch all appointments for auth.uid() ordered by starts_at
5- Split into Tabs: 'Upcoming' (starts_at > now(), status = confirmed) and 'Past'
6- Each appointment as a Card:
7 - Guest name (bold) and email (with mailto: link)
8 - Start time formatted in host's timezone: 'Tuesday, June 10 at 2:30 PM EDT'
9 - Meeting type name
10 - Duration as a Badge
11 - Guest notes (if present, shown in a lighter text block)
12 - Reference ID (first 8 chars of id, monospace)
13 - For upcoming: 'Cancel' Button with AlertDialog confirmation
14
15Cancellation:
16- UPDATE appointments SET status = 'cancelled' WHERE id = ?
17- Optimistic update: remove card from Upcoming list immediately
18
19Header area:
20- 'Copy booking link' Button that copies yourdomain.com/schedule/{slug} to clipboard
21- Stats row: total bookings this month, upcoming this week (simple count queries)

Pro tip: Add Supabase Realtime subscription on the appointments table for the current host. When a new confirmed appointment is inserted, show a toast 'New booking: [guest_name] at [time]'. This is more useful than email for hosts who keep the dashboard open.

Expected result: The dashboard shows appointments in two tabs. Stats show booking counts. Cancelling an appointment removes it from the upcoming list. The copy booking link button works.

Complete code

src/lib/computeSlots.ts
1import { parse, addMinutes, format, isAfter, isBefore, parseISO } from 'date-fns'
2
3type AvailabilitySlot = {
4 start_time: string // 'HH:mm'
5 end_time: string // 'HH:mm'
6}
7
8type ExistingAppointment = {
9 starts_at: string // ISO 8601 UTC
10 ends_at: string // ISO 8601 UTC
11}
12
13type TimeSlot = {
14 label: string // '2:30 PM'
15 isoUtc: string // UTC ISO string for storage
16}
17
18export function computeAvailableSlots(
19 date: Date,
20 availability: AvailabilitySlot,
21 durationMinutes: number,
22 existingAppointments: ExistingAppointment[]
23): TimeSlot[] {
24 const dateStr = format(date, 'yyyy-MM-dd')
25
26 // Parse availability times as local time on the given date
27 const windowStart = parse(
28 `${dateStr} ${availability.start_time}`,
29 'yyyy-MM-dd HH:mm',
30 new Date()
31 )
32 const windowEnd = parse(
33 `${dateStr} ${availability.end_time}`,
34 'yyyy-MM-dd HH:mm',
35 new Date()
36 )
37
38 const slots: TimeSlot[] = []
39 let cursor = windowStart
40 const now = new Date()
41
42 while (isBefore(cursor, windowEnd)) {
43 const slotEnd = addMinutes(cursor, durationMinutes)
44
45 // Skip slots in the past
46 if (isAfter(cursor, now)) {
47 // Check for conflicts with existing appointments
48 const hasConflict = existingAppointments.some((appt) => {
49 const apptStart = parseISO(appt.starts_at)
50 const apptEnd = parseISO(appt.ends_at)
51 return cursor < apptEnd && slotEnd > apptStart
52 })
53
54 if (!hasConflict) {
55 slots.push({
56 label: format(cursor, 'h:mm a'),
57 isoUtc: cursor.toISOString(),
58 })
59 }
60 }
61
62 cursor = addMinutes(cursor, durationMinutes)
63 }
64
65 return slots
66}

Customization ideas

Multiple meeting types on one scheduling page

Show a Select at the top of the scheduling page listing all active meeting_types for the host (e.g. '15-min quick call', '60-min strategy session'). When the guest selects a type, re-compute slots using that type's duration_minutes. This lets one host URL serve multiple appointment lengths.

Intake form questions per meeting type

Add a meeting_type_questions table with meeting_type_id, question_text, and is_required. The booking form dynamically renders these questions as Inputs or Textareas after the guest_notes field. Answers are stored as a JSONB column in appointments.

iCal and Google Calendar integration

Generate a .ics file from the appointment data using the VEVENT format and trigger a browser download from the confirmation page. For Google Calendar, construct a calendar.google.com/calendar/r/eventedit URL with date/time query parameters and open it in a new tab as a 'Add to Google Calendar' button.

Host unavailability overrides

Add a host_unavailability table for one-off dates or date ranges when the host is not available despite their normal schedule (vacations, holidays). The slot computation step checks this table and returns an empty slot list for any date that falls in an unavailability range.

Team round-robin scheduling

Add a teams table and team_members join table. A team booking page at /schedule/team/:slug distributes bookings round-robin across team members who have the requested time slot available. The current member index is tracked in a team_round_robin table and incremented on each successful booking.

Common pitfalls

Pitfall: Computing slots entirely client-side without any server validation

How to avoid: The partial unique index on appointments is your server-side safety net. Always handle the unique constraint violation (PostgreSQL error code 23505) on the client with a friendly error message and a prompt to re-select a slot.

Pitfall: Disabling the wrong days on the Calendar component

How to avoid: Initialize the Calendar with all dates disabled and only enable dates once the availability_slots query resolves. Use a loading skeleton on the Calendar while the query is in-flight: disabled={isLoading ? () => true : (date) => !hasAvailability(date)}.

Pitfall: Displaying times without clearly labeling the timezone

How to avoid: Always append the timezone abbreviation to every displayed time. Use Intl.DateTimeFormat with timeZoneName: 'short' to get the abbreviation. Show both host timezone and guest timezone on the confirmation screen.

Pitfall: Allowing guests to book slots that are fewer than a minimum lead time away

How to avoid: Add a minimum_notice_hours column to meeting_types. In the slot computation, filter out any slots where the slot start time is less than minimum_notice_hours from now().

Best practices

  • Use a partial unique index WHERE status = 'confirmed' on appointments(host_id, starts_at) as the database-level double-booking guard. This is the most reliable prevention mechanism regardless of application logic bugs.
  • Store all appointment timestamps as UTC timestamptz. Never store local time with an offset — use proper IANA timezone names separately for display purposes.
  • Make the guest booking page fully public (no auth) and use Supabase anon key for all reads. Only authenticated hosts can read their own appointments list.
  • Gracefully handle the 23505 unique constraint error from Supabase on the booking form submit. Show a human-readable message and automatically refetch the slot list to show updated availability.
  • Add an index on appointments(host_id, starts_at) to keep the 'fetch existing appointments for a date' query fast as appointment volume grows.
  • Generate a human-readable reference number for each appointment (first 8 chars of UUID, uppercased) to use in confirmations and cancellation requests rather than exposing the full UUID.
  • Validate the slug on the host settings page with a real-time availability check — query Supabase on blur to show 'Slug already taken' before the form is submitted.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a scheduling app where a host sets availability as recurring weekly time windows (stored as plain HH:mm times in their local timezone) and guests book 30-minute appointments. Help me write a TypeScript function that takes a target date, an availability window (start_time, end_time as strings), a duration in minutes, and an array of existing appointment objects with starts_at and ends_at as UTC ISO strings. The function should return an array of available slot objects with isoUtc (for storage) and label (formatted time in the guest's detected browser timezone). Show me how to handle the timezone conversion correctly using date-fns.

Lovable Prompt

Add an 'Upcoming bookings' section to my public scheduling page at /schedule/:slug. Below the calendar and time slots, show the next 3 confirmed appointments for this host as a simple list showing the date and time (no guest names for privacy). This acts as social proof that the host is active and in demand. Fetch this data from appointments WHERE host_id = ? AND starts_at > now() AND status = 'confirmed' ORDER BY starts_at LIMIT 3. Show only the formatted date and time, not any guest information.

Build Prompt

In Supabase, create a PostgreSQL function cancel_appointment(p_appointment_id uuid, p_guest_email text) that: (1) fetches the appointment by ID, (2) checks that the guest_email matches the stored email or that the caller is the host (auth.uid() = host_id), (3) checks that starts_at > now() + interval '1 hour' (no same-hour cancellations), (4) sets status = 'cancelled'. Make it SECURITY DEFINER callable by the anon role so guests can cancel without auth using their email as verification. Return a boolean success value.

Frequently asked questions

Does Lovable support public pages that do not require login?

Yes. Any route in your Lovable app can be made public. The guest scheduling page fetches data using the Supabase anon key, which only accesses data permitted by your RLS policies. Set SELECT policies on the hosts, meeting_types, availability_slots, and the bookings INSERT policy to public (anon role), and the page works for anyone without a login.

What is the difference between this and the booking-platform build?

This scheduling-app build focuses on the core double-booking constraint, UTC storage pattern, and a simple client-side slot computation approach. The booking-platform build adds a Supabase Edge Function for server-side slot computation (better for large scale), buffer time between bookings, and more complex service type configuration. Start with this build if you want the simpler version.

How does the partial unique index prevent double-bookings when two guests submit simultaneously?

PostgreSQL processes concurrent INSERTs serially for rows that would violate a unique constraint. The first INSERT succeeds; the second receives a unique constraint violation error (code 23505). Supabase surfaces this as an error response to the client. Your app catches this error and tells the guest to choose a different slot — the database is the final arbiter, not the application logic.

Can I add a buffer time between appointments so the host has a break?

Yes. Add a buffer_minutes column to meeting_types. In the slot computation, when checking for conflicts, treat each existing appointment as occupying starts_at to ends_at + buffer_minutes. A slot is unavailable if it starts before any existing appointment's buffered end time.

How do I let the host set different availability for each week, not just recurring?

Add a specific_date column to a separate availability_overrides table. When computing slots, first check if an override exists for the specific date and use that instead of the weekly template. This supports holiday exceptions and one-off schedule changes.

Is it possible for guests to reschedule instead of just cancelling?

Yes. Add a rescheduled_from_id column (references appointments) to appointments. The reschedule flow cancels the original appointment and creates a new one with rescheduled_from_id set to the original. You can then display reschedule history in the host dashboard and use the confirmation_code from the original booking to identify the rescheduled appointment.

Can I use this for team scheduling where any available team member takes the booking?

The current schema supports single-host scheduling. For round-robin team scheduling, you would need a teams table, team_members join table, and logic to find any team member available at the requested time. The booking is then assigned to the next team member in rotation. This is a significant extension of the current build.

Where can I get help if I need features beyond what this guide covers?

RapidDev builds production-grade Lovable applications. If you need complex scheduling logic, team features, payment integration, or reminder email workflows, reach out to discuss a scoped build.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.