Skip to main content
RapidDev - Software Development Agency
App Featuresvertical-tools19 min read

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

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.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

vertical-tools

Build with AI

4–8 hours with FlutterFlow or Lovable

Custom build

2–3 weeks custom dev

Running cost

$0–$35/mo (plus Stripe transaction fees per booking)

Works on

Mobile

Everything it takes to ship a Booking System — parts, prompts, and real costs.

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.

What users consider table stakes in 2026

  • Real-time availability display with booked slots visibly greyed out or removed — users should never select a slot only to receive a 'slot taken' error at submission
  • Slot selection with an intuitive date picker and time grid showing available windows for the selected day
  • Booking confirmation with an email including the appointment details and an iCal attachment for personal calendar sync
  • Cancellation and rescheduling self-service so users are not sending support emails to change a time
  • Buffer time between appointments visible in the availability grid — a provider's 15-minute cleanup gap should appear as unavailable to clients
  • Timezone-aware booking showing times in both the client's and provider's local timezone to prevent missed appointments

Anatomy of the Feature

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

Layers:UIDataBackendService

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.

Note: Set firstDay: DateTime.now() on the TableCalendar to prevent past date selection. Add enabledDayPredicate to grey out days where all slots are booked according to the DB query result.

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.

Note: A unique constraint on (provider_id, start_at) is a second defense layer — it catches concurrent inserts that slip past the Edge Function. Both defenses together make the system race-condition-proof.

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.

Note: Store all booking times as UTC timestamptz. Convert to the provider's and client's local timezone at display time only.

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.

Note: Triggering the confirmation email from the form submit (not the webhook) causes a race condition where users get a confirmation before Stripe has processed payment. Always trigger from the webhook.

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.

Note: The .ics download approach works without OAuth and takes 30 minutes to implement. The Google Calendar API push requires a Google Cloud project and OAuth consent screen setup.

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.

Note: Use constructEventAsync() in Deno Edge Functions (Lovable) instead of constructEvent() — the Deno runtime requires the async version for webhook signature verification.

The data model

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

schema.sql
1create table public.services (
2 id uuid primary key default gen_random_uuid(),
3 provider_id uuid references auth.users(id) on delete cascade not null,
4 name text not null,
5 duration_min int not null default 60,
6 buffer_min int not null default 0,
7 price_cents int not null default 0,
8 is_active bool not null default true,
9 created_at timestamptz not null default now()
10);
11
12alter table public.services enable row level security;
13
14create policy "Active services are viewable by all"
15 on public.services for select
16 using (is_active = true);
17
18create policy "Providers manage their own services"
19 on public.services for all
20 using (auth.uid() = provider_id);
21
22create table public.availability_blocks (
23 id uuid primary key default gen_random_uuid(),
24 provider_id uuid references auth.users(id) on delete cascade not null,
25 weekday int not null check (weekday between 0 and 6),
26 start_time time not null,
27 end_time time not null
28);
29
30alter table public.availability_blocks enable row level security;
31
32create policy "Availability blocks are viewable by all"
33 on public.availability_blocks for select
34 using (true);
35
36create policy "Providers manage their own availability"
37 on public.availability_blocks for all
38 using (auth.uid() = provider_id);
39
40create table public.bookings (
41 id uuid primary key default gen_random_uuid(),
42 service_id uuid references public.services(id) not null,
43 provider_id uuid references auth.users(id) not null,
44 client_id uuid references auth.users(id) on delete cascade not null,
45 start_at timestamptz not null,
46 end_at timestamptz not null,
47 status text not null default 'pending'
48 check (status in ('pending', 'confirmed', 'cancelled', 'no_show')),
49 stripe_payment_intent_id text,
50 cancellation_reason text,
51 created_at timestamptz not null default now(),
52 unique (provider_id, start_at)
53);
54
55alter table public.bookings enable row level security;
56
57create policy "Clients can view own bookings"
58 on public.bookings for select
59 using (auth.uid() = client_id);
60
61create policy "Providers can view their bookings"
62 on public.bookings for select
63 using (auth.uid() = provider_id);
64
65create policy "Clients can insert bookings"
66 on public.bookings for insert
67 with check (auth.uid() = client_id);
68
69create policy "Clients and providers can update bookings"
70 on public.bookings for update
71 using (auth.uid() = client_id OR auth.uid() = provider_id);
72
73create table public.availability_exceptions (
74 id uuid primary key default gen_random_uuid(),
75 provider_id uuid references auth.users(id) on delete cascade not null,
76 exception_date date not null,
77 is_blocked bool not null default true,
78 note text,
79 unique (provider_id, exception_date)
80);
81
82alter table public.availability_exceptions enable row level security;
83
84create policy "Exceptions are viewable by all"
85 on public.availability_exceptions for select
86 using (true);
87
88create policy "Providers manage their own exceptions"
89 on public.availability_exceptions for all
90 using (auth.uid() = provider_id);
91
92create index bookings_provider_start_idx on public.bookings (provider_id, start_at);
93create index bookings_client_id_idx on public.bookings (client_id);
94create index bookings_status_idx on public.bookings (status);

Heads up: 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 it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Native iOS + AndroidFit for this feature:

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.

Step by step

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

Where this path bites

  • 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

Third-party services you'll need

Four services power the full booking system. Stripe fees are the dominant variable cost as booking volume grows.

ServiceWhat it doesFree tierPaid from
StripePayment collection at booking time (deposit or full payment); webhook confirms payment before booking status changes to confirmedNo monthly fee2.9% + 30¢ per transaction
ResendBooking confirmation emails with iCal attachment; reminder emails sent 24h before the appointment3,000 emails/month$20/mo for 50K emails (approx)
Google Calendar APIOptional: add confirmed booking directly to the client's Google Calendar after OAuth consentFree for Calendar API basic usage; OAuth consent screen requiredFree (quota applies)
SupabasePostgreSQL database with atomic transactions for slot reservation, Auth, Edge Functions for the reservation lock and Stripe webhook handler, Realtime for live availability updates2 projects, 500MB database$25/mo Pro plan

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$2/mo

Supabase free tier covers the database at this scale. Resend free tier handles confirmation emails. Only cost is Stripe fees per transaction (2.9% + 30¢ per booking) — approximately $2–$5/mo for a low-volume service business.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Double-booking occurs when two users book the same slot simultaneously

Symptom: 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

Symptom: 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

Symptom: 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

Symptom: 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

Symptom: 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

1

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

2

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

3

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

4

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

5

Use constructEventAsync() in Deno Edge Functions for Stripe webhook signature verification — the synchronous version does not work in the Deno runtime Lovable uses

6

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

7

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

8

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

When You Need Custom Development

FlutterFlow and Lovable handle single-provider appointment booking well. These are the scenarios that require custom development:

  • Multi-provider scheduling with a shared availability pool — clients can book any available provider, and the system auto-assigns the earliest slot across the team roster
  • Class or group bookings with per-class capacity limits — yoga studio or cooking class where one session accommodates 12 participants simultaneously
  • Integration with an existing point-of-sale, salon management software, or CRM that requires real-time bidirectional sync
  • HIPAA-compliant healthcare appointment booking requiring encrypted data fields, access audit logs, and a Business Associate Agreement with your infrastructure provider

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

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.

RapidDev

Need this feature production-ready?

RapidDev builds a booking system into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

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.