Firebase Integration

Cloud Functions — Callable Functions from Flutter

16 min Lesson 10 of 13

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.auth is always available on the server.
  • Errors thrown as HttpsError on the server surface as typed FirebaseFunctionsException on the client.
  • No manual JSON encoding — you pass a Map and receive a Map.
  • Works seamlessly with the Firebase Local Emulator Suite for local development.
Note: Callable functions require the 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());
}
Tip: Run 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
Warning: Never throw a raw JavaScript 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.