Firebase Cloud Functions let you run server-side Node.js code triggered by HTTP calls, Firestore events, or schedules — all without a dedicated server. In FlutterFlow, you write the function code outside the visual editor (in the Firebase Functions directory), deploy via CLI, then call it from a FlutterFlow Custom Action using the firebase_functions package. Never write Cloud Function code in FlutterFlow's Custom Code panel — that panel is for client-side Dart only.
When You Need Cloud Functions in FlutterFlow
FlutterFlow handles most app logic visually — Firestore CRUD, auth, navigation, and simple API calls. Cloud Functions become necessary when you need server-side operations that the client cannot or should not perform: sending emails, processing payments, running expensive aggregations, using secret API keys securely, enforcing complex business logic that users should not be able to bypass, and reacting to database events. Understanding which function type to use for each scenario is the most important decision you'll make when adding server-side logic to a FlutterFlow app.
Prerequisites
- A FlutterFlow project with Firebase connected and on the Blaze (pay-as-you-go) plan
- Node.js 18+ and npm installed on your computer
- Firebase CLI installed globally: npm install -g firebase-tools
- Basic JavaScript familiarity — Cloud Functions are written in Node.js, not Dart
Step-by-step guide
Initialize Firebase Functions in your project directory
Initialize Firebase Functions in your project directory
Export your FlutterFlow project to your computer and open a terminal in the project root directory. Run firebase login to authenticate, then firebase init functions. Select 'Use an existing project' and choose your Firebase project. Select JavaScript (not TypeScript for simplicity, though TypeScript is recommended for larger projects). Accept the default ESLint configuration. When initialization completes, a functions/ directory appears in your project containing index.js (where all functions are defined), package.json, and node_modules. Open functions/index.js — this is where all your Cloud Functions live. The file starts with: const functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp();
Expected result: The functions/ directory exists with index.js, package.json, and a successfully installed node_modules folder.
Write an onCall function for secure client-triggered logic
Write an onCall function for secure client-triggered logic
onCall is the recommended function type for operations triggered by user actions in your app. It automatically handles authentication — you get the calling user's UID in context.auth.uid without any token verification code. Add a function to functions/index.js for a common use case like sending a welcome email or processing a payment. The function receives data (your custom parameters) and context (auth info). Always check context.auth is not null at the start of any onCall function that requires authentication. Return a plain JavaScript object — it's automatically serialized to JSON and returned to the calling app. Throw functions.https.HttpsError with a code and message for errors — these propagate back to the caller as structured exceptions.
1// functions/index.js — onCall function example2const functions = require('firebase-functions');3const admin = require('firebase-admin');4admin.initializeApp();56// Called from FlutterFlow Custom Action7exports.processUserAction = functions.https.onCall(8 async (data, context) => {9 // 1. Verify authentication10 if (!context.auth) {11 throw new functions.https.HttpsError(12 'unauthenticated',13 'You must be logged in to perform this action'14 );15 }16 // 2. Validate input parameters17 const { actionType, targetId } = data;18 if (!actionType || !targetId) {19 throw new functions.https.HttpsError(20 'invalid-argument',21 'actionType and targetId are required'22 );23 }24 // 3. Perform server-side logic25 const db = admin.firestore();26 const userId = context.auth.uid;27 await db.collection('userActions').add({28 userId,29 actionType,30 targetId,31 createdAt: admin.firestore.FieldValue.serverTimestamp(),32 });33 // 4. Return result34 return { success: true, message: 'Action recorded' };35 }36);Expected result: The function is defined in index.js. Running firebase deploy --only functions deploys it successfully and shows the function URL in the terminal output.
Add Firestore trigger and scheduled functions
Add Firestore trigger and scheduled functions
Firestore triggers run automatically when documents are created, updated, or deleted — no client call needed. Use them for: sending notifications when a new message is created, cleaning up related data when a user is deleted, or computing aggregations when new records are added. Scheduled functions run on a cron schedule — use them for: sending weekly email digests, expiring old records, generating daily reports. Both types are added to the same index.js file. Scheduled functions require the Cloud Scheduler API to be enabled in your Google Cloud Console (it's enabled automatically when you deploy a scheduled function for the first time).
1// Firestore trigger: runs when a new 'orders' document is created2exports.onOrderCreated = functions.firestore3 .document('orders/{orderId}')4 .onCreate(async (snap, context) => {5 const order = snap.data();6 const messaging = admin.messaging();7 // Get user's FCM token8 const userDoc = await admin.firestore()9 .collection('users')10 .doc(order.userId)11 .get();12 const fcmToken = userDoc.data()?.fcmToken;13 if (fcmToken) {14 await messaging.send({15 token: fcmToken,16 notification: {17 title: 'Order confirmed!',18 body: `Order #${context.params.orderId} received`,19 },20 });21 }22 });2324// Scheduled: runs every day at 9am UTC25exports.dailyCleanup = functions.pubsub26 .schedule('0 9 * * *')27 .timeZone('UTC')28 .onRun(async (_context) => {29 const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);30 const db = admin.firestore();31 const old = await db32 .collection('tempData')33 .where('createdAt', '<', cutoff)34 .get();35 const batch = db.batch();36 old.forEach((doc) => batch.delete(doc.ref));37 await batch.commit();38 console.log(`Cleaned up ${old.size} old records`);39 });Expected result: After deployment, the Firestore trigger fires automatically within 10-30 seconds of a matching document creation. The scheduled function runs on its cron schedule visible in Google Cloud Console.
Call Cloud Functions from FlutterFlow Custom Actions
Call Cloud Functions from FlutterFlow Custom Actions
Export your FlutterFlow project. In pubspec.yaml, ensure cloud_functions is listed as a dependency (it should be if Firebase is connected). Create a Custom Action in FlutterFlow named 'callProcessUserAction'. In the Custom Action code, import 'package:cloud_functions/cloud_functions.dart'. Create an instance: final functions = FirebaseFunctions.instance. Call the function using: final callable = functions.httpsCallable('processUserAction'). Pass data as a Map and await the result. Handle HttpsCallableException for error cases. Map the returned JSON data to your FlutterFlow variables by returning values as the appropriate Dart types.
1// FlutterFlow Custom Action: callProcessUserAction2import 'package:cloud_functions/cloud_functions.dart';34Future<bool> callProcessUserAction(5 String actionType,6 String targetId,7) async {8 try {9 final functions = FirebaseFunctions.instance;10 final callable = functions.httpsCallable(11 'processUserAction',12 options: HttpsCallableOptions(13 timeout: const Duration(seconds: 30),14 ),15 );16 final result = await callable.call({17 'actionType': actionType,18 'targetId': targetId,19 });20 // result.data is the Map returned by your Cloud Function21 return result.data['success'] == true;22 } on FirebaseFunctionsException catch (e) {23 // e.code contains the HttpsError code you threw24 // e.message contains the human-readable message25 print('Cloud Function error: ${e.code} - ${e.message}');26 return false;27 }28}Expected result: Calling the Custom Action from a FlutterFlow button tap executes the Cloud Function and returns the success boolean to the Action Flow.
Configure environment variables and secrets in Cloud Functions
Configure environment variables and secrets in Cloud Functions
Never hard-code API keys, database credentials, or secrets directly in Cloud Function code. Firebase offers two mechanisms: environment config (firebase functions:config:set) for non-sensitive values, and Secret Manager for sensitive values like API keys. For secrets, run: firebase functions:secrets:set SENDGRID_API_KEY. In your function code, access it with: process.env.SENDGRID_API_KEY (for 2nd gen functions) or functions.config().sendgrid.key (for 1st gen). Add --set-secrets=SENDGRID_API_KEY to your function definition for 2nd gen. For region configuration, set the region in your function: functions.region('us-central1') — match this to the region your Firestore is in to minimize latency.
1// Using Secret Manager with 2nd gen functions2const { onCall } = require('firebase-functions/v2/https');3const { defineSecret } = require('firebase-functions/params');45const sendgridKey = defineSecret('SENDGRID_API_KEY');67exports.sendWelcomeEmail = onCall(8 { secrets: [sendgridKey] },9 async (request) => {10 if (!request.auth) {11 throw new HttpsError('unauthenticated', 'Login required');12 }13 // Access secret value14 const apiKey = sendgridKey.value();15 // Use apiKey with SendGrid SDK...16 return { sent: true };17 }18);Expected result: Cloud Function deploys with secrets configured. The function can access the API key value without it appearing in your source code or logs.
Complete working example
1// functions/index.js — Complete example with all function types2// Run: firebase deploy --only functions34const functions = require('firebase-functions');5const admin = require('firebase-admin');6admin.initializeApp();78const db = admin.firestore();9const messaging = admin.messaging();1011// ─── onCall: called from FlutterFlow Custom Action ───────────────────────────12exports.processUserAction = functions.https.onCall(13 async (data, context) => {14 if (!context.auth) {15 throw new functions.https.HttpsError(16 'unauthenticated', 'Login required'17 );18 }19 const { actionType, targetId } = data;20 if (!actionType || !targetId) {21 throw new functions.https.HttpsError(22 'invalid-argument', 'Missing required parameters'23 );24 }25 await db.collection('userActions').add({26 userId: context.auth.uid,27 actionType,28 targetId,29 createdAt: admin.firestore.FieldValue.serverTimestamp(),30 });31 return { success: true };32 }33);3435// ─── Firestore trigger: new order → push notification ────────────────────────36exports.onOrderCreated = functions.firestore37 .document('orders/{orderId}')38 .onCreate(async (snap, context) => {39 const order = snap.data();40 const userDoc = await db.collection('users').doc(order.userId).get();41 const fcmToken = userDoc.data()?.fcmToken;42 if (!fcmToken) return null;43 return messaging.send({44 token: fcmToken,45 notification: {46 title: 'Order confirmed',47 body: `Order #${context.params.orderId} is being processed`,48 },49 data: { orderId: context.params.orderId },50 });51 });5253// ─── Scheduled: daily cleanup of temp records ────────────────────────────────54exports.dailyCleanup = functions.pubsub55 .schedule('0 9 * * *')56 .timeZone('UTC')57 .onRun(async () => {58 const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);59 const snap = await db60 .collection('tempData')61 .where('createdAt', '<', cutoff)62 .get();63 if (snap.empty) return null;64 const batch = db.batch();65 snap.forEach((doc) => batch.delete(doc.ref));66 await batch.commit();67 console.log(`Cleaned ${snap.size} records`);68 return null;69 });7071// ─── onRequest: public webhook endpoint ──────────────────────────────────────72exports.stripeWebhook = functions.https.onRequest(73 async (req, res) => {74 if (req.method !== 'POST') {75 return res.status(405).send('Method not allowed');76 }77 // Verify Stripe signature here78 const event = req.body;79 if (event.type === 'checkout.session.completed') {80 const session = event.data.object;81 await db.collection('payments').add({82 sessionId: session.id,83 amount: session.amount_total,84 currency: session.currency,85 customerId: session.customer,86 createdAt: admin.firestore.FieldValue.serverTimestamp(),87 });88 }89 res.status(200).json({ received: true });90 }91);Common mistakes
Why it's a problem: Writing Cloud Function code in FlutterFlow's Custom Code panel
How to avoid: Cloud Functions are Node.js code written in the functions/index.js file of your Firebase project, outside FlutterFlow entirely. Use FlutterFlow's Custom Code panel only for Dart code that calls the already-deployed functions, using the cloud_functions Flutter package.
Why it's a problem: Not checking context.auth in onCall functions before performing privileged operations
How to avoid: Add if (!context.auth) { throw new functions.https.HttpsError('unauthenticated', 'Login required'); } as the first line of every onCall function that requires authentication.
Why it's a problem: Deploying all functions with firebase deploy instead of targeted deploys during development
How to avoid: Deploy only the function you're working on: firebase deploy --only functions:functionName. This deploys a single function in 30-60 seconds.
Best practices
- Always return null from Firestore trigger and scheduled functions that don't need to return data — returning undefined causes warnings in Cloud Functions logs.
- Set function memory and timeout explicitly for functions that do heavy work: functions.runWith({ memory: '512MB', timeoutSeconds: 120 }) for aggregation tasks.
- Use Firestore transactions for any operation that reads then writes data based on the read value — without transactions, concurrent function executions can produce race conditions.
- Log meaningful messages inside functions using console.log for success states and console.error for failures — these appear in Google Cloud Logging and are invaluable for debugging.
- Use the Firebase Emulator Suite (firebase emulators:start) to test functions locally before deploying, completely avoiding billing for development and testing.
- Deploy functions to the same GCP region as your Firestore database to minimize latency on database operations inside functions.
- Add the RapidDev Engineering Team to your Firebase project as a viewer if you need expert help debugging a production function issue — having access to Cloud Logging is essential for diagnosis.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a FlutterFlow app with Firebase. I need a Firebase Cloud Function that sends a welcome email using SendGrid when a new user document is created in Firestore. The user document has fields: displayName, email, plan (free or premium). Write the complete Cloud Function code using the Firestore onCreate trigger, the SendGrid Node.js SDK, and Secret Manager for the API key. Include the sendgrid/mail npm install command and error handling.
I have a deployed Firebase Cloud Function named 'generateInvoice' that accepts: {orderId: string, userId: string} and returns: {invoiceUrl: string, invoiceId: string}. Write me a FlutterFlow Custom Action in Dart that calls this function using the cloud_functions package, handles the FirebaseFunctionsException error case, and returns the invoiceUrl string so I can open it in a WebView action in my app.
Frequently asked questions
Do I need the Firebase Blaze plan to use Cloud Functions?
Yes. Cloud Functions require the Blaze (pay-as-you-go) plan. The Spark (free) plan does not support Cloud Functions. However, Blaze includes a generous free tier: 2 million function invocations per month, 400,000 GB-seconds of compute, and 200,000 CPU-seconds. Most apps with under 10,000 active users stay within the free tier most months.
What is the difference between onCall and onRequest functions?
onCall functions are designed for app clients — they automatically verify Firebase Authentication tokens, handle CORS, and serialize/deserialize JSON. onRequest functions are raw HTTP endpoints — you handle everything manually and they can be called by any HTTP client (webhooks, cron jobs, external services). Use onCall for anything triggered from your app; use onRequest for webhooks from third-party services like Stripe.
How do I debug a Cloud Function that is failing in production?
Open Google Cloud Console → Cloud Logging → select your Firebase project. Filter by resource type 'Cloud Function' and look for console.error() output and unhandled exception traces. For faster iteration, test locally with the Firebase Emulator Suite (firebase emulators:start) which runs functions locally and shows logs in your terminal.
Can I call one Cloud Function from another Cloud Function?
Yes, using the Firebase Admin SDK's callable functions or via direct HTTP calls. However, calling functions from other functions creates complex dependency chains and makes error handling harder. As a general rule, prefer extracting shared logic into a regular JavaScript module that both functions import, rather than creating function-to-function call chains.
How long can a Cloud Function run before it times out?
First-generation functions have a maximum timeout of 540 seconds (9 minutes). Second-generation functions (recommended for new projects) support up to 3,600 seconds (60 minutes). Set the timeout explicitly in your function options — the default is 60 seconds for first-gen and 60 seconds for second-gen HTTP functions. For long-running tasks, consider breaking work into smaller batches and chaining function calls.
How do I handle sensitive data like payment information in Cloud Functions?
Never log sensitive data using console.log — Cloud Logging stores logs and they can be read by anyone with Cloud Logging access. Use Secret Manager for API keys. For PCI-compliant payment processing, never handle raw card numbers — use Stripe Elements or Payment Intents which handle card data entirely on Stripe's servers, and only pass payment intent IDs to your Cloud Functions.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation