# How to Add a Booking System to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

A booking system needs real-time slot availability from the database, an atomic reservation that prevents double-booking, timezone-aware storage (UTC timestamptz), a Stripe payment at booking time, and push reminders 24h before. With FlutterFlow or Lovable you can ship a working booking system in 4–8 hours. Running cost is $0–$5/month at launch (Stripe fees aside) and grows to $25–$35/month at 1,000 users when Supabase Pro is needed.

## What a Booking System Feature Actually Is

A booking system lets users select a service, pick an available date and time slot, pay a deposit or full amount, and receive a confirmation with a calendar invite. Under the hood it is a real-time availability engine, a race-condition-proof reservation write, and a notification pipeline. The feature sounds simple but has three landmines that break first builds: the double-booking race condition when two users grab the same slot simultaneously, timezone mismatches when the provider and client are in different cities, and Stripe webhook sequencing that sends confirmation emails before payment is actually confirmed. Getting these three right separates a trustworthy booking system from one that generates support tickets.

## Anatomy of the Feature

Six components. The slot reservation engine and the Stripe webhook sequencing are where first builds consistently fail under real conditions.

- **Availability calendar** (ui): FlutterFlow's TableCalendar widget with disabled dates for fully-booked days. A day detail view shows available time slots as a scrollable list with booked slots visually greyed out. Flutter's intl package formats times in the client's local timezone.
- **Slot reservation engine** (backend): A Supabase Edge Function handles the atomic slot reservation using SELECT ... FOR UPDATE NOWAIT inside a transaction. If another user has locked the same slot between the client viewing availability and submitting their booking, the function returns a conflict error. This is the only reliable way to prevent double-booking.
- **Booking data model** (data): Three tables: services (id, provider_id, name, duration_min, buffer_min, price_cents), availability_blocks (provider_id, weekday, start_time, end_time) for recurring availability, bookings (id, service_id, provider_id, client_id, start_at timestamptz, end_at timestamptz, status, stripe_payment_intent_id), and availability_exceptions (date, provider_id, is_blocked) for holidays and breaks.
- **Confirmation notifications** (service): Resend sends the booking confirmation email with an iCal (.ics) attachment triggered only from the Stripe webhook handler after payment is confirmed. flutter_local_notifications schedules a push reminder 24 hours before the appointment at booking time.
- **Calendar sync** (service): Optional but high-value: after booking is confirmed, offer a one-tap 'Add to Google Calendar' button that uses the Google Calendar API (with OAuth) or a downloadable .ics file. For mobile, EventKit on iOS and CalendarContract on Android via flutter_calendar_manager let users add the booking directly to their device calendar.
- **Payment at booking** (service): Stripe Payment Element collects deposit or full payment at the time of booking. A Supabase Edge Function verifies the PaymentIntent status before finalizing the booking status to 'confirmed'. The Stripe webhook handler (checkout.session.completed or payment_intent.succeeded) triggers the confirmation email via Resend.

## Data model

Four tables for services, availability, bookings, and exceptions with row-level security. Run in the Supabase SQL editor.

```sql
create table public.services (
  id uuid primary key default gen_random_uuid(),
  provider_id uuid references auth.users(id) on delete cascade not null,
  name text not null,
  duration_min int not null default 60,
  buffer_min int not null default 0,
  price_cents int not null default 0,
  is_active bool not null default true,
  created_at timestamptz not null default now()
);

alter table public.services enable row level security;

create policy "Active services are viewable by all"
  on public.services for select
  using (is_active = true);

create policy "Providers manage their own services"
  on public.services for all
  using (auth.uid() = provider_id);

create table public.availability_blocks (
  id uuid primary key default gen_random_uuid(),
  provider_id uuid references auth.users(id) on delete cascade not null,
  weekday int not null check (weekday between 0 and 6),
  start_time time not null,
  end_time time not null
);

alter table public.availability_blocks enable row level security;

create policy "Availability blocks are viewable by all"
  on public.availability_blocks for select
  using (true);

create policy "Providers manage their own availability"
  on public.availability_blocks for all
  using (auth.uid() = provider_id);

create table public.bookings (
  id uuid primary key default gen_random_uuid(),
  service_id uuid references public.services(id) not null,
  provider_id uuid references auth.users(id) not null,
  client_id uuid references auth.users(id) on delete cascade not null,
  start_at timestamptz not null,
  end_at timestamptz not null,
  status text not null default 'pending'
    check (status in ('pending', 'confirmed', 'cancelled', 'no_show')),
  stripe_payment_intent_id text,
  cancellation_reason text,
  created_at timestamptz not null default now(),
  unique (provider_id, start_at)
);

alter table public.bookings enable row level security;

create policy "Clients can view own bookings"
  on public.bookings for select
  using (auth.uid() = client_id);

create policy "Providers can view their bookings"
  on public.bookings for select
  using (auth.uid() = provider_id);

create policy "Clients can insert bookings"
  on public.bookings for insert
  with check (auth.uid() = client_id);

create policy "Clients and providers can update bookings"
  on public.bookings for update
  using (auth.uid() = client_id OR auth.uid() = provider_id);

create table public.availability_exceptions (
  id uuid primary key default gen_random_uuid(),
  provider_id uuid references auth.users(id) on delete cascade not null,
  exception_date date not null,
  is_blocked bool not null default true,
  note text,
  unique (provider_id, exception_date)
);

alter table public.availability_exceptions enable row level security;

create policy "Exceptions are viewable by all"
  on public.availability_exceptions for select
  using (true);

create policy "Providers manage their own exceptions"
  on public.availability_exceptions for all
  using (auth.uid() = provider_id);

create index bookings_provider_start_idx on public.bookings (provider_id, start_at);
create index bookings_client_id_idx on public.bookings (client_id);
create index bookings_status_idx on public.bookings (status);
```

The unique constraint on (provider_id, start_at) is the last line of defense against double-booking. The Edge Function with SELECT FOR UPDATE is the first. Both are necessary — the unique constraint catches concurrent inserts that race past the lock.

## Build paths

### Lovable — fit 3/10, 5–8 hours

Lovable's Supabase and Stripe native connectors handle the backend well, but the booking UI is web-first and feels less native than Flutter on mobile. Best for service businesses that primarily book via desktop browser.

1. Create a new Lovable project and connect Lovable Cloud so Supabase tables and auth are provisioned
2. Paste the prompt below and let Agent Mode build the service selection, slot picker, Stripe checkout, and confirmation email flow
3. Connect the Stripe connector in the Cloud tab and add STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET to Secrets
4. Test the double-booking scenario with two browser tabs booking the same slot simultaneously to verify the Edge Function blocks the second request
5. Publish and test Stripe payment flow on the live URL — Stripe Checkout does not work in the Lovable preview iframe

Starter prompt:

```
Build an appointment booking system. Supabase tables: services (id, provider_id, name, duration_min, buffer_min, price_cents), bookings (id, service_id, provider_id, client_id, start_at timestamptz, end_at timestamptz, status CHECK IN ('pending','confirmed','cancelled','no_show'), stripe_payment_intent_id), availability_blocks (provider_id, weekday 0-6, start_time, end_time), availability_exceptions (provider_id, exception_date, is_blocked). Client booking flow: step 1 — select a service from the provider's active services list; step 2 — show a date picker; step 3 — show available time slots for the selected date by computing available slots from availability_blocks minus existing bookings minus buffer time after each booking; step 4 — Stripe Checkout for payment (full price from services.price_cents). Slot reservation: write a Supabase Edge Function that uses SELECT FOR UPDATE NOWAIT to atomically reserve the slot before inserting the booking row — return 409 Conflict if already taken. After payment: trigger Stripe webhook handler in Edge Function that calls constructEventAsync() to verify signature, updates booking status to 'confirmed', and sends Resend confirmation email with .ics calendar attachment. Push reminder: schedule a push notification 24h before the appointment using the device's expo-notifications or web Push API. Cancellation: allow client to cancel up to 24h before; provider can cancel any time. Timezone: store all times as UTC timestamptz, display in viewer's local timezone. Handle edge cases: two users booking same slot simultaneously (409 from Edge Function), provider cancels after payment (show refund policy message), past date selection (disable in date picker), fully-booked day (grey out in calendar).
```

Limitations:

- Lovable generates a React web UI — mobile PWA booking feels less native than FlutterFlow on phones
- No native push notifications on iOS in a PWA — reminders only work on Android Chrome or via email
- The double-booking race condition requires explicit prompting of the Edge Function with SELECT FOR UPDATE — Lovable may generate a client-side check instead without this instruction

### Flutterflow — fit 5/10, 4–7 hours

The native mobile path and the right choice for service businesses. FlutterFlow's TableCalendar with real-time Supabase availability, Stripe Flutter SDK for payment, and flutter_local_notifications for reminders gives the most polished booking experience on iOS and Android.

1. Create a FlutterFlow project with Supabase integration; run the SQL schema from this page in the Supabase SQL editor
2. Add the TableCalendar widget to the booking page; bind the enabledDayPredicate to a Supabase query that returns fully-booked dates as disabled
3. Build the time slot list view from a Supabase query computing available slots from availability_blocks minus existing bookings; show booked slots greyed out
4. Integrate the Stripe Flutter SDK via Custom Action for the payment step; trigger the Supabase Edge Function after payment confirmation to finalize the booking
5. Add a flutter_local_notifications Custom Action to schedule the 24h reminder at the booking confirmation step

Limitations:

- Complex recurring availability rules with exceptions (e.g. different hours on holidays) require Custom Actions on the Pro plan ($70/mo)
- Stripe Flutter SDK requires a Custom Action which also needs Pro plan access
- iOS push notification permission must be explicitly requested before scheduling reminders — FlutterFlow does not generate this permission request automatically

### Custom — fit 4/10, 2–3 weeks

Justified when multi-provider scheduling with shared availability, class-based group bookings (yoga studio, cooking class), or integration with existing POS or salon software is required. Cal.com open-source is an adaptable base for complex booking logic.

1. Full Flutter development with comprehensive availability rule engine supporting multi-provider, recurring schedules with exception overrides
2. Class/group booking model with per-class capacity limits separate from one-on-one appointment logic
3. Cal.com open-source fork or direct integration via Cal.com API for complex availability management
4. HIPAA-compliant data handling for healthcare appointment booking — encrypted fields, audit log, and BAA with Supabase

Limitations:

- Cal.com self-hosted adds DevOps complexity and ongoing maintenance burden
- HIPAA compliance requires a Business Associate Agreement with Supabase (available on Enterprise tier) and additional implementation review

## Gotchas

- **Double-booking occurs when two users book the same slot simultaneously** — The most common AI-generated booking flow is: fetch available slots client-side, show the slot as available, then INSERT the booking row. When two users view the same available slot and both submit within milliseconds of each other, both reads see the slot as available and both INSERTs succeed — resulting in two confirmed bookings for the same time. Fix: Move the reservation to a Supabase Edge Function that runs SELECT ... FOR UPDATE NOWAIT inside a transaction before the INSERT. The first user's transaction locks the row; the second gets a lock conflict error and receives a 409 response. Add a unique constraint on (provider_id, start_at) as a second defense layer that catches any concurrent inserts that slip past the lock.
- **Stripe webhook fires after confirmation email already sent** — AI tools frequently wire the booking confirmation email to the form submit success response. Stripe's payment confirmation webhook fires asynchronously — typically within 2–5 seconds but occasionally delayed. Users receive a confirmation email before their payment is actually confirmed, and if the payment fails (card decline after initial authorization), they have a booking confirmation for an unpaid slot. Fix: Trigger the confirmation email exclusively from the Stripe webhook handler (on checkout.session.completed or payment_intent.succeeded), never from the form submit. In the Lovable Deno Edge Function, use constructEventAsync() for webhook signature verification — the synchronous constructEvent() does not work in Deno.
- **FlutterFlow TableCalendar shows past dates as bookable** — The default FlutterFlow TableCalendar configuration does not restrict past date selection. Users can select yesterday's date and reach the time slot picker, where no slots appear — creating a confusing dead end that looks like a bug. Fix: Set firstDay: DateTime.now() on the TableCalendar configuration to disable past dates. Additionally, add an enabledDayPredicate that queries booked slots from Supabase and disables fully-booked days — this prevents the equally confusing experience of selecting a booked day and seeing an empty slot list.
- **Timezone confusion: client in LA sees provider's NY appointment time as wrong** — When booking times are stored as local time strings ('2:00 PM') without timezone context, or as timestamps in the provider's local timezone, the client in a different timezone sees the wrong time. A 2:00 PM NY appointment appears as 2:00 PM to the LA client who then shows up at 11:00 AM NY time. Fix: Store all booking start_at and end_at as UTC timestamptz. On display, convert to each party's local timezone: use the tz Flutter package (timezone) to convert UTC to the client's timezone and separately to the provider's timezone. Show both timezones in the confirmation email to prevent ambiguity.
- **iOS push notification permission not requested before scheduling** — FlutterFlow auto-generates flutter_local_notifications scheduling code without generating the permission request flow first. On iOS, any attempt to schedule a notification without explicit user permission is silently ignored — the reminder never fires, and users miss their appointments with no warning. Fix: Add an explicit permission request action (using the permission_handler package) before the first notification is scheduled — ideally triggered at booking confirmation, with a clear message: 'Allow notifications to receive your appointment reminder 24 hours before.' iOS requires explicit user approval; the notification silently fails without it.

## Best practices

- Always enforce slot reservation atomically in a Supabase Edge Function using SELECT FOR UPDATE — client-side availability checks followed by a separate INSERT are exploitable by simultaneous users
- Trigger confirmation emails only from the Stripe webhook handler, never from the form submit response — send the email before payment is confirmed and you have an unbounded liability
- Store all booking times as UTC timestamptz and display in each user's local timezone — a timezone-ambiguous booking system is the fastest way to lose client trust
- Add a unique constraint on (provider_id, start_at) as a database-level second defense against double-booking, in addition to the Edge Function lock
- Use constructEventAsync() in Deno Edge Functions for Stripe webhook signature verification — the synchronous version does not work in the Deno runtime Lovable uses
- Request iOS push notification permission at booking confirmation time with a clear explanation — the reminder only works if the user has granted permission, and silent failure is invisible
- Send the iCal (.ics) attachment in the confirmation email so clients can add the appointment to their personal calendar in one click from any email client
- Grey out fully-booked days in the calendar before the user selects them — showing an empty time slot list after selecting a booked day creates confusion that looks like a bug

## Frequently asked questions

### How do I prevent double-booking?

Two defenses together make the system reliable. First, a Supabase Edge Function uses SELECT ... FOR UPDATE NOWAIT inside a transaction before inserting the booking — the first user's transaction locks the slot, and the second user receives a 409 Conflict error. Second, a unique constraint on (provider_id, start_at) catches any concurrent inserts that slip past the lock. Either defense alone can fail under timing pressure; both together are robust.

### Can I charge a deposit at booking time?

Yes. Use Stripe Checkout or Stripe Payment Element with the amount set to the deposit amount (e.g. 50% of the service price). Store the stripe_payment_intent_id in the bookings table. When the client arrives for the appointment, charge the remaining balance manually via the Stripe Dashboard or an in-app payment flow. Make your cancellation policy clear at booking time — Stripe's no-code refund tool handles refunds from the Dashboard.

### How do I handle cancellations and refunds?

Define a cancellation window in the services table (e.g. cancel_hours_before: 24). In your cancellation Edge Function, check if the current time is within the cancellation window. If it is, process the Stripe refund via the refunds.create API and update the booking status to 'cancelled'. If outside the window, apply your cancellation fee policy (no refund or partial). Always update the booking status in Supabase and send a Resend cancellation confirmation email.

### Can I sync bookings to Google Calendar?

Yes, two ways: the simple path is including an .ics file attachment in the Resend confirmation email — any calendar app opens it. The Google Calendar API path adds the event directly to the user's Google Calendar after OAuth consent, requiring a Google Cloud project. The .ics approach takes 30 minutes and works for 95% of users. The direct API approach takes 2–4 hours and requires the user to authorize your app with their Google account.

### How do I set buffer time between appointments?

Store buffer_min in the services table. When computing available slots, add the service duration_min plus buffer_min to each confirmed booking's start_at to get the next available slot. For example: a 60-minute appointment with a 15-minute buffer makes the provider unavailable from 2:00 PM to 3:15 PM, so the next slot can only start at 3:15 PM. This logic runs in the Edge Function or in your slot-generation query.

### What's the best way to handle timezones in a booking app?

Store all booking times as UTC timestamptz in Supabase — never store local time strings. When displaying times, convert UTC to the viewer's local timezone using the tz Flutter package (timezone library). Show both the client's local time and the provider's local time in the confirmation email to prevent ambiguity. The single biggest source of missed appointments in booking apps is timezone-ambiguous stored times.

### Can I add a waiting list for fully-booked slots?

Yes. Add a waitlist table with (slot_start_at, provider_id, user_id, position, created_at). When a cancellation occurs, an Edge Function queries the waitlist for that slot and offers the opening to the first person in queue via a Resend email with a time-limited booking link (token expires in 4 hours). The offered user clicks the link, which triggers the normal payment and reservation flow.

### How do I send reminder notifications before appointments?

At booking confirmation time, schedule two reminders: a push notification via flutter_local_notifications set to fire 24 hours before start_at, and a Resend email triggered by a pg_cron job that runs nightly checking for bookings starting within 24 hours. The push notification requires explicit iOS permission — request it at the booking confirmation step with a clear explanation. The email reminder works without any additional permissions.

---

Source: https://www.rapidevelopers.com/app-features/booking-system
© RapidDev — https://www.rapidevelopers.com/app-features/booking-system
