Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Build a Booking System with Calendar Integration in FlutterFlow

Build a service booking system using Firestore collections for services and bookings. Display availability on a table_calendar Custom Widget with green, yellow, and red day markers. Show available time slots in a GridView, grey out booked ones, and use a Firestore transaction to atomically reserve a slot and prevent double-booking. Trigger a confirmation email via Cloud Function on successful booking.

What you'll learn

  • How to model services and bookings in Firestore with time slot availability
  • How to integrate the table_calendar package as a Custom Widget with day-level availability coloring
  • How to display and filter available time slots in a tappable GridView
  • How to prevent double-booking with Firestore transactions
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read25-35 minFlutterFlow Free+ (Custom Widget required for table_calendar)March 2026RapidDev Engineering Team
TL;DR

Build a service booking system using Firestore collections for services and bookings. Display availability on a table_calendar Custom Widget with green, yellow, and red day markers. Show available time slots in a GridView, grey out booked ones, and use a Firestore transaction to atomically reserve a slot and prevent double-booking. Trigger a confirmation email via Cloud Function on successful booking.

Service booking with visual calendar and conflict-free reservations

This tutorial builds a full service booking flow: a Firestore data model for services and bookings, a calendar Custom Widget showing availability per day using color coding, a time slot grid that greys out already-booked slots, and a Firestore transaction that atomically reserves a slot to prevent two users from booking the same time. A Cloud Function sends a confirmation email after each successful booking.

Prerequisites

  • A FlutterFlow project with Firebase/Firestore connected
  • Firebase Authentication enabled with users signing in
  • Basic understanding of Custom Widgets in FlutterFlow
  • Cloud Functions enabled for email confirmations (Firebase Blaze plan)

Step-by-step guide

1

Create the services and bookings Firestore collections

In Firestore, create a services collection with fields: name (String), durationMinutes (Integer — 30, 60, or 90), price (Double), providerId (String — the staff member), and description (String). Create a bookings collection with fields: serviceId (String), userId (String), providerId (String), startTime (Timestamp), endTime (Timestamp), status (String — confirmed, cancelled), and createdAt (Timestamp). Add 2-3 test services. Set Firestore rules: users can read services, authenticated users can create bookings, and users can only read/cancel their own bookings.

Expected result: Firestore has services and bookings collections with test data and proper security rules.

2

Build the table_calendar Custom Widget with availability coloring

Create a Custom Widget named BookingCalendar that imports the table_calendar package. The widget accepts parameters: providerId (String) and an Action Parameter callback onDateSelected (DateTime). Inside the widget, query bookings for the selected month and providerId. For each day, calculate availability: green if zero bookings for that day, yellow if some slots remain, red if all slots are booked (compare booking count against total available slots based on working hours and service duration). Use TableCalendar's calendarBuilders.defaultBuilder to return a Container with circular background in the appropriate color. On day tap, call the onDateSelected callback passing the tapped date.

booking_calendar.dart
1// Custom Widget: BookingCalendar
2import 'package:table_calendar/table_calendar.dart';
3
4TableCalendar(
5 firstDay: DateTime.now(),
6 lastDay: DateTime.now().add(Duration(days: 90)),
7 focusedDay: _focusedDay,
8 selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
9 onDaySelected: (selected, focused) {
10 setState(() { _selectedDay = selected; _focusedDay = focused; });
11 widget.onDateSelected?.call(selected);
12 },
13 calendarBuilders: CalendarBuilders(
14 defaultBuilder: (context, day, focused) {
15 final color = _getAvailabilityColor(day);
16 return Container(
17 margin: const EdgeInsets.all(4),
18 decoration: BoxDecoration(color: color, shape: BoxShape.circle),
19 alignment: Alignment.center,
20 child: Text('${day.day}'),
21 );
22 },
23 ),
24)

Expected result: A calendar widget displays with green, yellow, and red day markers based on booking availability.

3

Display available time slots in a GridView

Below the calendar, add a GridView that shows available time slots for the selected date. Create a Custom Function generateTimeSlots that takes the provider's startHour, endHour, and service durationMinutes, then generates all possible slot start times (e.g., 9:00, 9:30, 10:00... for 30-min slots). Run a Backend Query on bookings for the selected providerId and date to get already-booked startTimes. For each generated slot, compare against booked times: if booked, render a grey disabled Container with strikethrough text; if available, render a tappable blue Container. Bind the On Tap of available slots to a Page State selectedSlot (Timestamp). Each slot Container shows the formatted time (e.g., '10:30 AM').

Expected result: A grid of time slots appears when a date is selected. Booked slots are greyed out; available slots are tappable and highlight on selection.

4

Book the selected slot using a Firestore transaction

Add a Confirm Booking button below the time slot grid. On Tap, trigger a Custom Action that runs a Firestore transaction: inside the transaction, query bookings where providerId == selected provider AND startTime == selectedSlot. If any booking exists (another user booked it in the meantime), abort and show a SnackBar 'This slot was just booked. Please choose another.' If no conflict, create the booking document with serviceId, userId (currentUser.uid), providerId, startTime (selectedSlot), endTime (selectedSlot + durationMinutes), status: confirmed, and createdAt: now. The transaction ensures atomicity — if two users tap Book at the same millisecond, only one succeeds.

book_slot_transaction.dart
1// Custom Action: bookSlotWithTransaction
2final firestore = FirebaseFirestore.instance;
3await firestore.runTransaction((transaction) async {
4 final conflict = await firestore
5 .collection('bookings')
6 .where('providerId', isEqualTo: providerId)
7 .where('startTime', isEqualTo: selectedSlot)
8 .where('status', isEqualTo: 'confirmed')
9 .get();
10 if (conflict.docs.isNotEmpty) {
11 throw Exception('Slot already booked');
12 }
13 transaction.set(firestore.collection('bookings').doc(), {
14 'serviceId': serviceId,
15 'userId': currentUser.uid,
16 'providerId': providerId,
17 'startTime': selectedSlot,
18 'endTime': Timestamp.fromDate(
19 selectedSlot.toDate().add(Duration(minutes: duration))),
20 'status': 'confirmed',
21 'createdAt': FieldValue.serverTimestamp(),
22 });
23});

Expected result: Booking is created atomically. If two users try the same slot simultaneously, only one succeeds and the other sees an error message.

5

Send confirmation email and show booking details

Deploy a Cloud Function triggered by Firestore onCreate on the bookings collection. The function reads the booking data, fetches the user's email and the service name, then sends a confirmation email using SendGrid or Firebase Extensions Trigger Email. The email includes the service name, provider, date, time, and a cancellation link. In FlutterFlow, after the transaction succeeds, navigate to a BookingConfirmationPage that queries the newly created booking document and displays: service name, provider name, date and time formatted, and a Cancel Booking button. The cancel button updates the booking status to cancelled and shows a confirmation SnackBar.

Expected result: User sees a confirmation page with booking details and receives an email. They can cancel the booking which updates the status in Firestore.

Complete working example

Booking System Architecture
1Firestore Data Model:
2 services/{serviceId}
3 name: String ("Haircut")
4 durationMinutes: Integer (30)
5 price: Double (35.00)
6 providerId: String
7 description: String
8 providers/{providerId}
9 name: String ("Jane Smith")
10 startHour: Integer (9)
11 endHour: Integer (17)
12 workingDays: List<Integer> ([1,2,3,4,5])
13 bookings/{bookingId}
14 serviceId: String
15 userId: String
16 providerId: String
17 startTime: Timestamp (2026-03-30 10:00)
18 endTime: Timestamp (2026-03-30 10:30)
19 status: String ("confirmed" | "cancelled")
20 createdAt: Timestamp
21
22BookingPage Layout:
23 AppBar: "Book a Service"
24 DropDown (service selector sets Page State selectedService)
25 BookingCalendar Custom Widget
26 Props: providerId, onDateSelected callback
27 table_calendar with DayBuilder
28 Green circle: 0 bookings on day
29 Yellow circle: partial availability
30 Red circle: fully booked
31 On Day Tap callback sets Page State selectedDate
32 Text ("Available Times for {selectedDate}")
33 GridView (crossAxisCount: 4)
34 Time Slot Container
35 Available: blue bg, white text, tappable
36 Booked: grey bg, strikethrough text, disabled
37 Selected: dark blue bg, bold white text
38 Divider
39 Summary Row: Service + Date + Time + Price
40 ElevatedButton ("Confirm Booking")
41 On Tap Custom Action: Firestore Transaction
42 Check for conflicts abort if exists
43 Create booking doc Navigate to Confirmation

Common mistakes when building a Booking System with Calendar Integration in FlutterFlow

Why it's a problem: Not using a Firestore transaction for booking — two users book the same slot simultaneously

How to avoid: Always use a Firestore transaction that atomically reads existing bookings for the slot and writes the new booking only if no conflict exists. If a conflict is detected inside the transaction, abort and show the user a message.

Why it's a problem: Showing all 24 hours as available slots instead of filtering by provider working hours

How to avoid: Store provider working hours (startHour, endHour, workingDays) and only generate time slots within those bounds. Skip weekends if workingDays excludes them.

Why it's a problem: Loading all bookings for all dates on calendar render

How to avoid: Query bookings scoped to the visible month range: startTime >= firstDayOfMonth AND startTime <= lastDayOfMonth. Update the query when the user navigates to a different month.

Best practices

  • Use Firestore transactions to prevent double-booking — never create bookings with simple writes
  • Scope booking queries to the visible month to avoid loading historical data unnecessarily
  • Store provider working hours separately and use them to generate valid time slots only
  • Add a buffer between slots (e.g., 5 minutes) to account for setup time between appointments
  • Send confirmation emails via Cloud Functions triggered by Firestore onCreate
  • Allow cancellation only up to a configurable cutoff (e.g., 24 hours before the appointment)
  • Test with concurrent users to verify the transaction correctly prevents double-booking

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Design a Firestore data model for a service booking system with services, providers (with working hours), and bookings collections. Write a Dart Firestore transaction that checks for conflicting bookings before creating a new one. Include the query for finding time slot conflicts.

FlutterFlow Prompt

Create a booking page with a calendar at the top showing available dates in green and fully booked dates in red. Below the calendar, show a grid of 30-minute time slots for the selected date. Booked slots should be greyed out. Add a Confirm Booking button at the bottom.

Frequently asked questions

How do I prevent double-booking in FlutterFlow?

Use a Firestore transaction in a Custom Action. Inside the transaction, query existing bookings for the same provider and time slot. If any exist, abort the transaction and show an error. If none exist, create the booking document. Transactions are atomic — only one concurrent booking attempt can succeed.

Can I use FlutterFlow's built-in DateTimePicker instead of table_calendar?

Yes, for a simpler approach. The DateTimePicker works for selecting a date without visual availability coloring. You lose the green/yellow/red day markers but avoid the Custom Widget complexity. Show availability feedback after the date is selected via the time slot grid.

How do I handle different service durations in the time slot grid?

Pass the selected service's durationMinutes to the slot generation function. For a 30-minute service, generate slots every 30 minutes. For a 60-minute service, generate slots every 60 minutes. Check that each potential slot does not overlap with any existing booking in that duration window.

How do I add a cancellation policy with a time cutoff?

On the Cancel button, add a Conditional Action: if booking.startTime minus 24 hours > DateTime.now(), allow cancellation (update status to cancelled). Otherwise, show a SnackBar explaining the 24-hour cancellation cutoff policy. Adjust the 24-hour window to match your business rules.

Can I send SMS reminders before appointments?

Yes. Deploy a scheduled Cloud Function that runs hourly, queries bookings with startTime within the next hour, and sends SMS via Twilio API to each user's phone number. Store phone numbers on user documents.

Can RapidDev help build a booking system with staff scheduling and recurring appointments?

Yes. Complex booking systems with multi-staff scheduling, recurring appointments, buffer times, resource allocation, and calendar sync (Google Calendar / Outlook) require Cloud Functions and third-party integrations. RapidDev can architect the complete solution.

RapidDev

Talk to an Expert

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

Book a free consultation

Need help with your project?

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.