Cloud Functions — Callable Functions from Flutter
Cloud Functions — Callable Functions from Flutter
Firebase Cloud Functions let you run secure server-side logic without managing infrastructure. HTTPS Callable Functions are a special type that can be invoked directly from a Flutter app using a typed SDK — they automatically handle authentication context, serialisation, and structured error propagation. In this lesson you will deploy a Node.js callable function and invoke it from Dart.
What Makes Callable Functions Different?
Unlike regular HTTPS functions triggered by a URL, callable functions communicate through the Firebase Functions SDK. This gives you several advantages:
- The SDK automatically includes the signed-in user's ID token, so
context.authis always available on the server. - Errors thrown as
HttpsErroron the server surface as typedFirebaseFunctionsExceptionon the client. - No manual JSON encoding — you pass a
Mapand receive aMap. - Works seamlessly with the Firebase Local Emulator Suite for local development.
firebase_functions Flutter package and Node.js 18+ (or 20+) on the server side. The server runtime is deployed via the Firebase CLI.Step 1 — Write the Node.js Callable Function
Inside your Firebase project's functions/index.js (or src/index.ts), define the callable function using onCall:
functions/index.js — Server-Side Callable Function
// Node.js 18+ / Firebase Functions v2
const { onCall, HttpsError } = require('firebase-functions/v2/https');
const { getMessaging } = require('firebase-admin/messaging');
const admin = require('firebase-admin');
admin.initializeApp();
exports.sendWelcomeNotification = onCall(async (request) => {
// 1. Enforce authentication
if (!request.auth) {
throw new HttpsError(
'unauthenticated',
'You must be signed in to call this function.'
);
}
const { deviceToken, userName } = request.data;
// 2. Validate input
if (!deviceToken || typeof deviceToken !== 'string') {
throw new HttpsError('invalid-argument', 'deviceToken is required.');
}
// 3. Perform server-side logic (send FCM notification)
const message = {
token: deviceToken,
notification: {
title: `Welcome, ${userName ?? 'Friend'}!`,
body: 'Thanks for joining. Explore the app now.',
},
data: { screen: 'home' },
};
const messageId = await getMessaging().send(message);
// 4. Return a typed response
return { success: true, messageId };
});
Deploy the function with the Firebase CLI:
Terminal — Deploy to Firebase
# From the root of your Firebase project
firebase deploy --only functions:sendWelcomeNotification
Step 2 — Add the Flutter Dependency
Add firebase_functions to pubspec.yaml:
pubspec.yaml
dependencies:
firebase_core: ^3.6.0
firebase_auth: ^5.3.0
firebase_functions: ^5.1.0
Step 3 — Invoke the Callable Function from Flutter
Use FirebaseFunctions.instance.httpsCallable() to get a reference, then call it with a data map. Always await the result inside a try/catch to handle FirebaseFunctionsException:
lib/services/functions_service.dart
import 'package:cloud_functions/cloud_functions.dart';
class FunctionsService {
final FirebaseFunctions _functions = FirebaseFunctions.instance;
/// Calls the [sendWelcomeNotification] Cloud Function.
/// Returns the messageId on success or throws [FirebaseFunctionsException].
Future<String> sendWelcomeNotification({
required String deviceToken,
required String userName,
}) async {
final callable = _functions.httpsCallable('sendWelcomeNotification');
try {
final HttpsCallableResult result = await callable.call<Map<String, dynamic>>({
'deviceToken': deviceToken,
'userName': userName,
});
final data = Map<String, dynamic>.from(result.data as Map);
return data['messageId'] as String;
} on FirebaseFunctionsException catch (e) {
// Typed error from HttpsError thrown on the server
throw Exception('[${e.code}] ${e.message}');
}
}
}
Step 4 — Call the Service from a Widget
lib/screens/home_screen.dart — Triggering the Function
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool _loading = false;
String? _result;
Future<void> _triggerNotification() async {
setState(() { _loading = true; _result = null; });
try {
final service = FunctionsService();
final messageId = await service.sendWelcomeNotification(
deviceToken: 'DEVICE_FCM_TOKEN_HERE',
userName: 'Edrees',
);
setState(() { _result = 'Sent! Message ID: $messageId'; });
} catch (e) {
setState(() { _result = 'Error: $e'; });
} finally {
setState(() { _loading = false; });
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Cloud Functions Demo')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: _loading ? null : _triggerNotification,
child: _loading
? const CircularProgressIndicator()
: const Text('Send Welcome Notification'),
),
if (_result != null) ...[
const SizedBox(height: 16),
Text(_result!, textAlign: TextAlign.center),
],
],
),
),
);
}
}
Using the Local Emulator
During development, point the SDK at the local Functions emulator instead of production:
main.dart — Connect to the Emulator
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// Only in debug / emulator mode
if (kDebugMode) {
FirebaseFunctions.instance.useFunctionsEmulator('localhost', 5001);
}
runApp(const MyApp());
}
firebase emulators:start --only functions and set useFunctionsEmulator so you can iterate without deploying. Changes to your Node.js code take effect after restarting the emulator.Error Handling Reference
The server throws HttpsError with a status code string. On the client, FirebaseFunctionsException.code maps to the same string:
- unauthenticated — caller is not signed in
- permission-denied — caller lacks required role/claim
- invalid-argument — malformed input data
- not-found — requested resource does not exist
- internal — unhandled server-side exception
Error from a callable function — it surfaces as internal with no message. Always use new HttpsError(code, message) to give the client actionable error information.Summary
Callable functions bridge Flutter and Firebase's server-side runtime cleanly: the SDK handles auth token injection, serialisation, and typed error propagation. Deploy with firebase deploy --only functions, invoke with httpsCallable().call(data), and catch FirebaseFunctionsException for structured error handling. Use the local emulator during development to iterate quickly without incurring Cloud Functions invocation costs.