Build a tutoring marketplace in FlutterFlow with tutor profile listings, subject search, calendar-based booking, Agora video calls secured by per-session server-generated tokens, a shared whiteboard via Firestore real-time sync, and Stripe Checkout for per-session payment. Never use a single Agora token for all sessions.
The Architecture of a Secure Tutoring Marketplace
A tutoring platform has three distinct experiences in one app: students browsing and booking tutors, tutors managing their schedule and earnings, and both parties joining a live video session. The most security-critical component is video call access. Agora video calls are identified by a channel name and secured by tokens. If you generate one token per tutor or per subject and reuse it across sessions, any user who intercepts or guesses the channel name can join any call — including sessions between other users. The correct pattern is one token per session, generated by a Cloud Function at the moment of booking, stored in the session document, and invalidated after the session ends. This tutorial builds that secure pattern alongside the full marketplace experience.
Prerequisites
- A FlutterFlow project on the Pro plan with code export enabled
- An Agora account with an App ID and App Certificate (from console.agora.io)
- A Firebase project with Firestore, Authentication, and Cloud Functions (Blaze plan)
- A Stripe account connected via a Cloud Function (secret key stored in Firebase Secret Manager)
Step-by-step guide
Design the Firestore schema for tutors, sessions, and bookings
Design the Firestore schema for tutors, sessions, and bookings
Your Firestore structure needs four collections. The tutors collection extends users with fields: displayName, avatarUrl, subjects (Array of String), hourlyRate (Number), rating (Number), reviewCount (Number), bio (String), availableSlots (Array of Maps with startTime and endTime Timestamps), and stripeAccountId for payout routing. The sessions collection is the central booking record: studentId, tutorId, subjectId, scheduledAt (Timestamp), durationMinutes (Number), status (String: Pending / Confirmed / In Progress / Completed / Cancelled), agoraChannelName (String, unique per session), agoraToken (String, generated at booking time), paymentIntentId (String), amountCents (Number), and whiteboardId (String). Create a whiteboards subcollection under each session document for real-time stroke syncing. This schema keeps all session-critical data — including the unique video token — in one document.
Expected result: Your Firestore has tutors, sessions, and a whiteboards structure visible in the Firebase console.
Build the tutor discovery and booking flow
Build the tutor discovery and booking flow
Create a TutorBrowsePage with a search TextField bound to a Firestore query on the tutors collection filtered by subjects arrayContains the search term. Display results in a ListView with tutor avatar, name, subjects row, star rating, and hourly rate. Tapping a tutor opens TutorProfilePage showing their full bio, reviews from a reviews subcollection, and an availability calendar. The calendar is a horizontal scrollable list of date chips for the next 14 days. Selecting a date shows the available time slots for that day (filtered from the tutor's availableSlots array). Tapping a slot opens a booking confirmation dialog showing the total cost (durationMinutes / 60 * hourlyRate). Tapping 'Book and Pay' calls a Cloud Function that creates the session document, generates the Agora token, creates a Stripe Payment Intent, and returns the client secret for Stripe Checkout.
Expected result: Students can search for tutors by subject, view profiles with availability, select a slot, and reach the payment confirmation screen.
Generate per-session Agora tokens in a Cloud Function
Generate per-session Agora tokens in a Cloud Function
Create a Cloud Function called createTutoringSession. It accepts studentId, tutorId, scheduledAt, durationMinutes, and subjectId. Inside the function: generate a UUID for the channel name, use the Agora Access Token library (agora-access-token npm package) to generate a token with the channel name, your App ID, and App Certificate — set the token expiry to the session duration plus 30 minutes. Generate a Stripe Payment Intent for the session amount. Create the session document in Firestore with all fields including agoraChannelName, agoraToken (encrypted at rest by Firestore), paymentIntentId, and status: 'Pending'. Return the session document ID and Stripe client secret to the caller. The Agora App Certificate must be stored in Firebase Secret Manager — never in client code or plain function config.
1// functions/index.js2const functions = require('firebase-functions');3const admin = require('firebase-admin');4const { RtcTokenBuilder, RtcRole } = require('agora-access-token');5const Stripe = require('stripe');6const { v4: uuidv4 } = require('uuid');7admin.initializeApp();89exports.createTutoringSession = functions.https.onCall(async (data, context) => {10 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');1112 const { tutorId, scheduledAt, durationMinutes, subjectId, amountCents } = data;13 const studentId = context.auth.uid;1415 const appId = process.env.AGORA_APP_ID;16 const appCertificate = process.env.AGORA_APP_CERTIFICATE;17 const stripeSecretKey = process.env.STRIPE_SECRET_KEY;1819 // Generate unique channel name20 const channelName = uuidv4().replace(/-/g, '');2122 // Generate per-session Agora token (expires at session end + 30 min buffer)23 const expirySeconds = Math.floor(Date.now() / 1000) + (durationMinutes + 30) * 60;24 const token = RtcTokenBuilder.buildTokenWithUid(25 appId, appCertificate, channelName, 0, RtcRole.PUBLISHER, expirySeconds26 );2728 // Create Stripe Payment Intent29 const stripe = Stripe(stripeSecretKey);30 const paymentIntent = await stripe.paymentIntents.create({31 amount: amountCents,32 currency: 'usd',33 metadata: { studentId, tutorId, channelName },34 });3536 // Create session document37 const sessionRef = await admin.firestore().collection('sessions').add({38 studentId,39 tutorId,40 subjectId,41 scheduledAt: new Date(scheduledAt),42 durationMinutes,43 status: 'Pending',44 agoraChannelName: channelName,45 agoraToken: token,46 paymentIntentId: paymentIntent.id,47 amountCents,48 createdAt: admin.firestore.FieldValue.serverTimestamp(),49 });5051 return {52 sessionId: sessionRef.id,53 clientSecret: paymentIntent.client_secret,54 };55});Expected result: Calling the Cloud Function returns a session ID and Stripe client secret. The session document in Firestore contains a unique channel name and token. Different session documents always have different channel names.
Build the video call screen using Agora in a Custom Widget
Build the video call screen using Agora in a Custom Widget
Add agora_rtc_engine: ^6.3.2 to your pubspec dependencies. Create a Custom Widget called AgoraVideoCallWidget. In initState, initialize RtcEngine with your Agora App ID. Read the agoraChannelName and agoraToken from the session document passed as parameters. Call engine.joinChannelWithUserAccount() with the token, channel name, and current user UID. Add two RtcLocalView and RtcRemoteView widgets inside a Stack — the remote view fills the screen, and the local view appears as a small picture-in-picture in the corner. Add floating action buttons for mute audio, toggle camera, flip camera, and end call. When end call is tapped, call engine.leaveChannel() and update the session status to 'Completed' in Firestore. The video widget reads the token from Firestore — never from a client-side constant.
Expected result: Both student and tutor can join the video call by navigating to the session and tapping 'Join Call'. They see each other's video with audio and camera controls.
Add the shared whiteboard with Firestore real-time sync
Add the shared whiteboard with Firestore real-time sync
Create a WhiteboardWidget Custom Widget. Use Flutter's GestureDetector to capture pan gestures as drawing strokes. Each stroke is a list of Offset points. When the user lifts their finger, save the stroke as a new document in sessions/{sessionId}/whiteboards/{strokeId} with fields: userId, color (hex), strokeWidth (double), points (Array of Maps with x and y doubles), and timestamp. Subscribe to the whiteboards subcollection with a Firestore real-time listener. On each new document, add the stroke to a local list and repaint using a CustomPainter. Both tutor and student see strokes appear in real time as each other draws. Add a 'Clear All' button that deletes all stroke documents in a batch. Add color and brush-size selection at the top of the widget.
Expected result: When either participant draws on the whiteboard, the strokes appear on the other's screen within 1-2 seconds. Clearing the whiteboard removes all strokes for both participants simultaneously.
Complete working example
1import 'package:cloud_firestore/cloud_firestore.dart';2import 'package:cloud_functions/cloud_functions.dart';3import 'package:firebase_auth/firebase_auth.dart';45// --- Create a tutoring session and get Stripe client secret ---6Future<Map<String, String>?> createTutoringSession({7 required String tutorId,8 required DateTime scheduledAt,9 required int durationMinutes,10 required String subjectId,11 required int amountCents,12}) async {13 if (FirebaseAuth.instance.currentUser == null) return null;1415 final callable = FirebaseFunctions.instance16 .httpsCallable('createTutoringSession');1718 final result = await callable.call({19 'tutorId': tutorId,20 'scheduledAt': scheduledAt.toIso8601String(),21 'durationMinutes': durationMinutes,22 'subjectId': subjectId,23 'amountCents': amountCents,24 });2526 return {27 'sessionId': result.data['sessionId'] as String,28 'clientSecret': result.data['clientSecret'] as String,29 };30}3132// --- Load session with Agora credentials for joining video ---33Future<Map<String, dynamic>?> loadSessionForCall(String sessionId) async {34 final uid = FirebaseAuth.instance.currentUser?.uid;35 if (uid == null) return null;3637 final doc = await FirebaseFirestore.instance38 .collection('sessions')39 .doc(sessionId)40 .get();4142 if (!doc.exists) return null;43 final data = doc.data()!;4445 // Verify the caller is the student or tutor46 if (data['studentId'] != uid && data['tutorId'] != uid) return null;4748 return {49 'channelName': data['agoraChannelName'],50 'token': data['agoraToken'],51 'sessionId': sessionId,52 'status': data['status'],53 };54}5556// --- Update session status ---57Future<void> updateSessionStatus(String sessionId, String status) async {58 await FirebaseFirestore.instance59 .collection('sessions')60 .doc(sessionId)61 .update({62 'status': status,63 'updatedAt': FieldValue.serverTimestamp(),64 if (status == 'Completed') 'completedAt': FieldValue.serverTimestamp(),65 });66}Common mistakes
Why it's a problem: Generating a single Agora token for all tutoring sessions or all sessions with the same tutor
How to avoid: Generate a new UUID channel name and a new Agora token for every session at the moment of booking from a Cloud Function. Store the token in the session document and restrict Firestore read access to only the student and tutor on that session.
Why it's a problem: Storing the Agora App Certificate or Stripe secret key in the Flutter client code
How to avoid: Store all secret keys in Firebase Secret Manager and access them only from Cloud Functions. Client-side code should never have access to these values.
Why it's a problem: Sending whiteboard stroke data on every onPanUpdate event
How to avoid: Collect all points in the current stroke in memory during onPanUpdate. Save the complete stroke as a single Firestore document only when onPanEnd fires. This reduces writes from potentially thousands to one per stroke.
Best practices
- Always generate Agora tokens server-side with a per-session expiry — never reuse tokens across sessions
- Store Agora App Certificate, Stripe secret key, and all sensitive credentials exclusively in Firebase Secret Manager
- Set Firestore security rules so only the session's studentId and tutorId can read the session document and its agoraToken field
- Throttle whiteboard writes to onPanEnd events — collect points in memory, write once per completed stroke
- Confirm payment before confirming the session booking — use a Stripe webhook to update session status to 'Confirmed' after payment succeeds
- Send calendar reminders via FCM 24 hours and 15 minutes before each scheduled session
- Archive session recordings or session notes in Firebase Storage after completion so students can review them later
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a tutoring platform in FlutterFlow. Write a Firebase Cloud Function that generates a per-session Agora RTC token using the agora-access-token npm package. The function accepts a channel name, user UID, and session duration in minutes. The token should expire 30 minutes after the session ends. Store the App ID and App Certificate in Firebase Secret Manager, not as hardcoded strings.
In my FlutterFlow project, write a Dart Custom Action called loadSessionForCall that accepts a sessionId String, reads the sessions/{sessionId} Firestore document, verifies the current user is either the studentId or tutorId field, and returns a Map containing agoraChannelName, agoraToken, and status. Return null if the user is unauthorized.
Frequently asked questions
Do I need an Agora account to add video calls to my FlutterFlow app?
Yes. Agora is a video SDK that requires registration at console.agora.io. You will need your App ID (public) and App Certificate (private, server-side only). Agora offers 10,000 free minutes per month which is sufficient for testing and early-stage launch.
Why must Agora tokens be generated server-side and not in the Flutter app?
Token generation requires the Agora App Certificate, which is a secret credential that should never be in client-side code. If a user decompiles your app and finds the App Certificate, they can generate valid tokens to join any Agora channel on your account. Always generate tokens in a Cloud Function.
Can I build this tutoring platform on FlutterFlow's Free plan?
No. The video call feature requires Custom Code with the agora_rtc_engine package, which needs a Pro plan with code export. Cloud Functions also require Firebase Blaze (pay-as-you-go) plan. You can prototype the booking and profile UI on the Free plan, but the video and payment integration requires upgrading.
How do I handle a tutor no-showing for a session?
Track session status — if the tutor does not join within 10 minutes of the scheduled start time, a Cloud Function triggered by a scheduled job should automatically update the session status to 'Tutor No Show' and trigger a full refund via Stripe's refund API, then send the student an FCM notification.
How do payouts to tutors work with Stripe?
Use Stripe Connect (Express accounts) for tutor payouts. During tutor onboarding, redirect them through Stripe's Connect onboarding flow to create a connected account. Store their stripeAccountId in Firestore. When a session is completed, use Stripe's transfer or payout API to send their share of the session fee (minus your platform commission) to their connected account.
Can two students join the same tutoring session (group lessons)?
Yes. Agora supports multiple participants in one channel. Change the session model to allow studentIds as an Array instead of a single studentId. Generate one token per session but allow multiple users to join with the same channel name. Adjust the payment flow to charge each student individually and increase the tutor's payout accordingly.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation