Connect FlutterFlow to any calendar service (Google Calendar, Outlook, Calendly) via a Cloud Function that handles OAuth and proxies API calls. Sync events to Firestore for fast offline display. Render events in a table_calendar Custom Widget with colored date markers. Create events via a DateTimePicker form that POSTs to the calendar API. Cache events every 15 minutes to avoid hitting API rate limits.
Build a calendar integration that works with Google Calendar, Outlook, or Calendly using a reusable Cloud Function pattern
Every scheduling, booking, or productivity app needs a calendar integration — but calendar APIs are among the most complex OAuth flows to implement, and calling them from mobile client code exposes user tokens. This tutorial teaches a generic pattern that works for any calendar service: a Cloud Function handles the OAuth token exchange and proxies all calendar API calls, events are synced to Firestore for instant display, and a table_calendar Custom Widget renders the visual calendar. Once you have this pattern working for one calendar provider, adding a second is a matter of adding new Cloud Function routes.
Prerequisites
- A Firebase project with Cloud Functions enabled (Blaze plan)
- A calendar service account or developer app credentials (Google Cloud project with Calendar API enabled, or Microsoft Azure app registration for Outlook)
- Node.js installed locally for Cloud Function development
- A FlutterFlow project where you want to add the calendar feature
Step-by-step guide
Build the Cloud Function proxy for your calendar service
Build the Cloud Function proxy for your calendar service
Create a Cloud Function named calendarProxy that accepts POST requests from FlutterFlow. The function takes an action parameter that specifies the operation: listEvents (fetch events in a date range), createEvent (create a new event), updateEvent (modify an existing event), deleteEvent (remove an event). For Google Calendar: use the googleapis npm package with a service account (for app-owned calendars) or OAuth user credentials (for user-owned calendars). For Outlook/Microsoft Graph: use @microsoft/microsoft-graph-client with an access token from MSAL. The function maps each action to the appropriate calendar API endpoint, handles token refresh transparently, and returns a normalized event format: {id, title, startDateTime, endDateTime, location, description, calendarId}. This normalized format means your FlutterFlow UI does not need to change if you switch or add calendar providers — only the Cloud Function changes.
1// functions/index.js — Generic Calendar API proxy2// Install: cd functions && npm install axios googleapis3const functions = require('firebase-functions');4const admin = require('firebase-admin');5const { google } = require('googleapis');67admin.initializeApp();89// Create OAuth2 client from stored credentials10function getGoogleAuthClient() {11 const cfg = functions.config().google_calendar;12 const auth = new google.auth.OAuth2(13 cfg.client_id,14 cfg.client_secret,15 cfg.redirect_uri16 );17 auth.setCredentials({18 access_token: cfg.access_token,19 refresh_token: cfg.refresh_token,20 });21 return auth;22}2324exports.calendarProxy = functions.https.onRequest(async (req, res) => {25 res.set('Access-Control-Allow-Origin', '*');26 if (req.method === 'OPTIONS') { res.status(204).send(''); return; }2728 const { action, calendarId = 'primary', eventId, eventData, timeMin, timeMax } = req.body;29 const auth = getGoogleAuthClient();30 const calendar = google.calendar({ version: 'v3', auth });31 const db = admin.firestore();3233 try {34 let result;3536 if (action === 'listEvents') {37 const response = await calendar.events.list({38 calendarId,39 timeMin: timeMin || new Date().toISOString(),40 timeMax: timeMax || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),41 maxResults: 100,42 singleEvents: true,43 orderBy: 'startTime',44 });45 // Normalize and cache events in Firestore46 const events = (response.data.items || []).map((e) => ({47 id: e.id,48 title: e.summary || 'No title',49 startDateTime: e.start.dateTime || e.start.date,50 endDateTime: e.end.dateTime || e.end.date,51 location: e.location || '',52 description: e.description || '',53 calendarId,54 }));55 // Write to Firestore for offline access56 const batch = db.batch();57 events.forEach((event) => {58 batch.set(db.collection('calendar_events').doc(event.id), {59 ...event,60 cachedAt: admin.firestore.FieldValue.serverTimestamp(),61 });62 });63 batch.set(db.collection('calendar_meta').doc('sync'), {64 lastSyncedAt: admin.firestore.FieldValue.serverTimestamp(),65 });66 await batch.commit();67 result = events;6869 } else if (action === 'createEvent') {70 const response = await calendar.events.insert({71 calendarId,72 requestBody: {73 summary: eventData.title,74 description: eventData.description,75 location: eventData.location,76 start: { dateTime: eventData.startDateTime, timeZone: eventData.timeZone || 'UTC' },77 end: { dateTime: eventData.endDateTime, timeZone: eventData.timeZone || 'UTC' },78 attendees: eventData.attendees?.map((e) => ({ email: e })) || [],79 },80 });81 const created = response.data;82 // Add to Firestore immediately83 await db.collection('calendar_events').doc(created.id).set({84 id: created.id, title: created.summary,85 startDateTime: created.start.dateTime,86 endDateTime: created.end.dateTime,87 location: created.location || '',88 calendarId,89 cachedAt: admin.firestore.FieldValue.serverTimestamp(),90 });91 result = created;9293 } else if (action === 'deleteEvent') {94 await calendar.events.delete({ calendarId, eventId });95 await db.collection('calendar_events').doc(eventId).delete();96 result = { success: true };97 }9899 res.json({ success: true, data: result });100 } catch (err) {101 console.error('Calendar CF error:', err.message);102 res.status(500).json({ success: false, error: err.message });103 }104});Expected result: Cloud Function is deployed. A POST with {action: 'listEvents'} returns events from your calendar and writes them to Firestore calendar_events collection.
Build the table_calendar Custom Widget to display events
Build the table_calendar Custom Widget to display events
Add the table_calendar Custom Widget to display events as markers on a calendar. In FlutterFlow: Custom Code → Pubspec Dependencies → add table_calendar at the current version. Add a Custom Widget named CalendarView. Parameters: events (List of JSON — event objects from Firestore), onDaySelected (Action — called when user taps a date), selectedDay (DateTime — the currently highlighted day). The widget renders a full-month calendar with dots under dates that have events. It calls onDaySelected when the user taps a date, allowing FlutterFlow to update a Page State variable with the selected date and filter the event list below the calendar. The calendar displays different colored dots for different event types if you include a type field in the event data.
1// Custom Widget: CalendarView2// Pubspec: table_calendar: ^3.1.03// Parameters:4// events: List<dynamic> (JSON event objects)5// selectedDay: DateTime67import 'package:flutter/material.dart';8import 'package:table_calendar/table_calendar.dart';910class CalendarViewWidget extends StatefulWidget {11 final List<dynamic> events;12 final DateTime selectedDay;13 final Future Function(DateTime) onDaySelected;1415 const CalendarViewWidget({16 required this.events,17 required this.selectedDay,18 required this.onDaySelected,19 Key? key,20 }) : super(key: key);2122 @override23 State<CalendarViewWidget> createState() => _CalendarViewWidgetState();24}2526class _CalendarViewWidgetState extends State<CalendarViewWidget> {27 late DateTime _focusedDay;28 late DateTime _selectedDay;2930 @override31 void initState() {32 super.initState();33 _focusedDay = widget.selectedDay;34 _selectedDay = widget.selectedDay;35 }3637 // Get events for a specific day38 List<dynamic> _getEventsForDay(DateTime day) {39 return widget.events.where((event) {40 try {41 final start = DateTime.parse(event['startDateTime'] as String);42 return start.year == day.year &&43 start.month == day.month &&44 start.day == day.day;45 } catch (_) { return false; }46 }).toList();47 }4849 @override50 Widget build(BuildContext context) {51 return TableCalendar(52 firstDay: DateTime.now().subtract(const Duration(days: 365)),53 lastDay: DateTime.now().add(const Duration(days: 365)),54 focusedDay: _focusedDay,55 selectedDayPredicate: (day) => isSameDay(_selectedDay, day),56 eventLoader: _getEventsForDay,57 calendarStyle: const CalendarStyle(58 markerDecoration: BoxDecoration(59 color: Color(0xFF1E88E5),60 shape: BoxShape.circle,61 ),62 selectedDecoration: BoxDecoration(63 color: Color(0xFF1E88E5),64 shape: BoxShape.circle,65 ),66 todayDecoration: BoxDecoration(67 color: Color(0xFF90CAF9),68 shape: BoxShape.circle,69 ),70 ),71 onDaySelected: (selected, focused) {72 setState(() {73 _selectedDay = selected;74 _focusedDay = focused;75 });76 widget.onDaySelected(selected);77 },78 );79 }80}Expected result: CalendarView Custom Widget renders a full monthly calendar. Dates with events show blue dot markers. Tapping a date calls the onDaySelected callback.
Build the calendar page with event list and Firestore data binding
Build the calendar page with event list and Firestore data binding
Create a CalendarPage in FlutterFlow. Add a Column with two sections: top half is the CalendarView Custom Widget, bottom half is a ListView of events for the selected day. Add a Backend Query on the page to read from the Firestore calendar_events collection, ordered by startDateTime ascending. Add Page State variables: selectedDate (DateTime, initial: today), selectedDayEvents (List of JSON, initial: empty). Pass the Backend Query result (full list) to the CalendarView widget's events parameter. On day selection (onDaySelected action from CalendarView), update Page State selectedDate = selected date, then update selectedDayEvents = filter the Backend Query result to only events on the selected date using a Custom Function filterEventsByDate(events, date). The ListView below the calendar is bound to the selectedDayEvents Page State list. Each list item shows a time range, title, and location in a Container with a left border colored by the event type.
Expected result: Calendar page shows the monthly calendar with event markers. Tapping a date filters the list below to show only that day's events.
Create the event creation form with DateTimePicker
Create the event creation form with DateTimePicker
Add a FloatingActionButton on the CalendarPage. On Tap: Show Bottom Sheet containing the event creation form. The form has: a TextField for event title, a TextField for location, a TextField for description, two DateTimePicker widgets for start and end datetime (pre-populated with the selected date from Page State), a TextField for attendee emails (comma-separated), and a Save button. Store form values in Page State variables: newEventTitle (String), newEventStart (DateTime), newEventEnd (DateTime), newEventLocation (String). The Save button Action Flow: validate that title is not empty and end is after start → call the calendarProxy API with {action: 'createEvent', eventData: {title, startDateTime, endDateTime, location, attendees}} → on success, dismiss the bottom sheet, show a SnackBar 'Event created', and refresh the Backend Query on the parent page using Refresh on Action.
Expected result: FAB opens an event creation form. Submitting the form calls the Cloud Function to create a real calendar event and immediately shows it in the calendar.
Set up a scheduled sync to keep Firestore cache current
Set up a scheduled sync to keep Firestore cache current
Calendar events change over time — users create events from other devices, invitations arrive, or events are rescheduled. Without a sync mechanism, the Firestore cache goes stale. Create a Cloud Function with a Cloud Scheduler trigger that runs every 15 minutes: functions.pubsub.schedule('every 15 minutes').onRun(). This function calls the same listEvents logic as the calendarProxy but runs server-side on a schedule, updating the Firestore calendar_events collection automatically. In FlutterFlow, the real-time Backend Query on calendar_events picks up changes within 1-2 seconds of the scheduled sync writing to Firestore. Add a visible sync status on the CalendarPage: read calendar_meta/sync → lastSyncedAt → format as 'Synced 3 minutes ago' using a Custom Function. This shows users the data freshness without requiring them to manually refresh.
Expected result: Firestore calendar_events is automatically updated every 15 minutes. A 'Synced X minutes ago' label shows the last sync time.
Complete working example
1Cloud Function: calendarProxy (HTTPS onRequest)2=================================================3Actions:4 listEvents → calendar.events.list → sync to Firestore5 createEvent → calendar.events.insert → add to Firestore6 updateEvent → calendar.events.update → update Firestore7 deleteEvent → calendar.events.delete → delete from Firestore89Normalized event format (same for all calendar providers):10 id, title, startDateTime (ISO 8601), endDateTime (ISO 8601)11 location, description, calendarId, attendees[]1213Cloud Function: syncCalendar (Pub/Sub scheduled: every 15 min)14 Calls listEvents for next 30 days15 Batch-writes to Firestore calendar_events16 Updates calendar_meta/sync.lastSyncedAt1718Firestore Schema19=================20calendar_events/{googleEventId}21 id, title, startDateTime, endDateTime22 location, description, calendarId, cachedAt2324calendar_meta/sync25 lastSyncedAt: Timestamp2627FlutterFlow Architecture28=========================29CalendarPage30 Backend Query: calendar_events (real-time, order by startDateTime)31 Page State:32 selectedDate: DateTime (today)33 selectedDayEvents: List<JSON>34 isCreatingEvent: Boolean3536 Column:37 ├── CalendarView Custom Widget38 │ params:39 │ events = Backend Query result40 │ selectedDay = selectedDate Page State41 │ onDaySelected →42 │ Update selectedDate = selected43 │ Update selectedDayEvents = filterEventsByDate(events, selected)44 │45 ├── Text (selectedDate formatted as 'Monday, March 25')46 │47 └── ListView (bound to selectedDayEvents Page State)48 └── EventCard Component49 ├── Left border (colored)50 ├── Text (HH:mm – HH:mm time range)51 ├── Text (title, bold)52 └── Text (location, grey)5354 FAB → Show Bottom Sheet (create event form)55 Fields: title, start DateTime, end DateTime, location56 Submit → calendarProxy createEvent API57 → Refresh Backend Query5859Custom Function: filterEventsByDate(List events, DateTime date)60 Returns events where startDateTime.date == date.date6162Custom Widget: CalendarView (table_calendar)63 Params: events List<JSON>, selectedDay DateTime64 Action: onDaySelected(DateTime)Common mistakes
Why it's a problem: Calling the calendar API directly from FlutterFlow on every page load to fetch events, without caching in Firestore
How to avoid: Sync events to Firestore on a Cloud Scheduler schedule (every 15 minutes). Read from Firestore in FlutterFlow for instant 50ms display. Only call the calendar API directly for user-initiated writes (create, update, delete) where real-time accuracy is required.
Why it's a problem: Not handling timezone correctly — storing event start/end times in local time without timezone information and displaying them incorrectly for users in different timezones
How to avoid: Store event times as full ISO 8601 strings with timezone offset in Firestore. Use Dart's DateTime.parse() which handles timezone offsets. Convert to local time for display using .toLocal(). Pass the user's timezone (from device locale via Global Properties or a user preference) in the createEvent API call to ensure events are created in the correct timezone.
Why it's a problem: Using the same Cloud Function URL for both the initial OAuth redirect and the API proxy, without handling the two different request types
How to avoid: Create separate Cloud Functions for different concerns: calendarAuth (handles OAuth redirect and token exchange), calendarProxy (handles API calls using stored tokens), and syncCalendar (scheduled sync). Each function has a single, clear responsibility.
Best practices
- Cache events in Firestore on a schedule and use Firestore as the primary read source — this provides offline support, instant display, and protects against calendar API rate limits
- Normalize event data to a consistent format in the Cloud Function — this lets you switch calendar providers or add a second one without changing FlutterFlow UI code
- Always store event times as full ISO 8601 strings with timezone information — never strip timezone data from calendar event timestamps
- Use the table_calendar package for the calendar UI — it is the most feature-complete Flutter calendar widget with event markers, range selection, and format switching
- Implement the sync status display (last synced X minutes ago) so users know the data freshness without needing to pull-to-refresh manually
- Add a pull-to-refresh gesture on the event list that triggers an immediate Cloud Function sync for users who need current data
- For multi-user apps, store events per-user in Firestore under users/{uid}/calendar_events rather than a shared collection
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow app with Firebase and need to integrate Google Calendar API. Write a Firebase Cloud Function in Node.js using the googleapis package that handles these actions via POST: listEvents (fetch events for a date range, normalize to {id, title, startDateTime, endDateTime, location}), createEvent (POST to calendar.events.insert with title, start, end, location, attendees), and deleteEvent (delete by eventId). Also write a Cloud Scheduler function that syncs events every 15 minutes to a Firestore calendar_events collection.
Create a CalendarPage with a Backend Query on the Firestore calendar_events collection ordered by startDateTime. Add a CalendarView Custom Widget at the top half bound to the query results, and a ListView at the bottom half filtered by the selected date using a Page State variable. Add a FAB that opens a bottom sheet with a form for title, start time, end time, and location using DateTimePicker widgets.
Frequently asked questions
Can I integrate with Calendly instead of Google Calendar?
Yes — Calendly has a REST API at api.calendly.com/v2 with Bearer token authentication. Create a Cloud Function that proxies to Calendly's GET /scheduled_events endpoint to list events. Create new booking links via POST /scheduling_links. The pattern is the same: Cloud Function handles auth, normalizes the response format, writes to Firestore. Calendly also supports webhooks for new booking notifications — register the Cloud Function URL as a webhook to get real-time updates when users book or cancel.
How do I let individual users connect their own Google Calendar?
Implement the OAuth web flow: create a Cloud Function that generates a Google OAuth authorization URL with calendar scope and the user's Firebase UID as state parameter. Launch URL from FlutterFlow to open Google's login page. Google redirects to your callback Cloud Function with an auth code. Exchange the code for access and refresh tokens. Store the tokens in Firestore under users/{uid}/calendar_token (encrypted or using Firebase Secret Manager). Subsequent calendarProxy calls retrieve the user-specific tokens instead of a service account.
What is the difference between Google Calendar API and CalDAV?
Google Calendar API is a REST API specific to Google — it supports all Google Calendar features and is the easiest to integrate via Node.js googleapis package. CalDAV is an open standard protocol that Google Calendar, Apple Calendar, and other providers support — it allows a single client to work with multiple calendar providers. CalDAV is more complex to implement and requires a dedicated CalDAV library. For FlutterFlow apps targeting only Google Calendar users, use the Google Calendar REST API. For apps needing to support multiple calendar providers, CalDAV or a calendar aggregation service like Cronofy is worth the added complexity.
Can I add video conferencing links (Google Meet, Zoom) to created events?
Yes. For Google Meet: in the createEvent Cloud Function, add conferenceDataVersion: 1 and conferenceData: {createRequest: {requestId: uuidv4(), conferenceSolutionKey: {type: 'hangoutsMeet'}}} to the event.insert call. Google automatically generates a Meet link and attaches it. The Meet URL is in response.data.conferenceData.entryPoints[0].uri. For Zoom: use the Zoom API to create a meeting first (POST /users/me/meetings), get the join URL, then add it as a Google Calendar event description or custom location.
How do I handle recurring events in the calendar?
Google Calendar returns recurring events with a recurrence rule (RRULE) in the event object — for example, RRULE:FREQ=WEEKLY;BYDAY=MO. When you call events.list with singleEvents: true (as in this tutorial's Cloud Function), Google expands recurring events into individual instances for the requested date range — each occurrence appears as a separate event in the list. This is the correct approach for displaying calendars. If you need to edit recurring events, the series ID is in event.recurringEventId.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation