← Back to posts Cover image for Flutter Hybrid Backend: Seamlessly Integrating Firebase Auth with Your Custom Server

Flutter Hybrid Backend: Seamlessly Integrating Firebase Auth with Your Custom Server

· 7 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

When building a Flutter app, you often find yourself at a crossroads for your backend: go all-in with a platform like Firebase, or build a custom server from scratch? Both paths have distinct advantages. Firebase offers quick setup and ease of use, especially for user authentication and real-time data. A custom server, on the other hand, gives you complete control over your data, business logic, and infrastructure—crucial for highly specific and complex needs.

But what if you could harness the strengths of both? This is where the hybrid backend approach becomes really useful. It allows you to leverage Firebase Authentication for its robust, secure, and easy-to-implement user management, while delegating all other data storage and intricate business logic to your own custom server. It’s a strong combination that provides both convenience and flexibility.

The Core Idea: How It Works

The secret sauce of this hybrid model lies in Firebase ID Tokens. Here’s the simplified flow:

  1. User Authentication (Flutter App): Your Flutter application uses the Firebase SDK to sign in a user (e.g., via email/password, Google, Apple, etc.).
  2. ID Token Acquisition (Flutter App): Upon successful authentication, Firebase issues an ID Token. This is a JSON Web Token (JWT) that cryptographically verifies the user’s identity.
  3. Authenticated Request (Flutter App to Custom Server): When your Flutter app needs to interact with your custom backend, it includes this Firebase ID Token in the Authorization header of its HTTP requests.
  4. Token Verification (Custom Server): Your custom backend receives the request, extracts the ID Token, and uses the Firebase Admin SDK to verify its authenticity and integrity. This SDK ensures the token hasn’t been tampered with and was genuinely issued by Firebase.
  5. User Identification & Authorization (Custom Server): Once verified, the Admin SDK provides the user’s unique Firebase User ID (UID) and other claims. Your server can then use this UID to identify the user, fetch their specific data from your custom database, and apply business logic, confident that the user is legitimately authenticated.

Client-Side (Flutter) Setup

First, ensure Firebase is set up in your Flutter project and firebase_auth is initialized. Your main.dart might look something like this:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart'; // Generated by FlutterFire CLI

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

Next, let’s create a simple AuthService to handle user sign-in and retrieve the Firebase ID token.

import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  // Sign in with email and password
  Future<User?> signIn(String email, String password) async {
    try {
      UserCredential userCredential = await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
      return userCredential.user;
    } on FirebaseAuthException catch (e) {
      // Handle specific Firebase authentication errors
      if (e.code == 'user-not-found') {
        print('No user found for that email.');
      } else if (e.code == 'wrong-password') {
        print('Wrong password provided for that user.');
      } else {
        print('Firebase Auth Error: ${e.message}'); // Added for general auth errors
      }
      // Re-throw to propagate the error to the UI for user feedback
      rethrow;
    } catch (e) {
      print('An unexpected error occurred during sign-in: $e');
      rethrow;
    }
  }

  // Get the current user's ID token. Forces a refresh to ensure it's valid.
  Future<String?> getCurrentUserToken() async {
    User? user = _auth.currentUser;
    if (user != null) {
      // Passing `true` forces a token refresh if it's expired or near expiration.
      return await user.getIdToken(true);
    }
    return null;
  }

  // Sign out
  Future<void> signOut() async {
    await _auth.signOut();
  }
}

Now, we’ll create an ApiClient that uses this token for making authenticated requests to your custom backend.

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:your_app/services/auth_service.dart'; // Adjust path as needed

class ApiClient {
  final String _baseUrl = "https://api.your-custom-backend.com"; // Your backend URL
  final AuthService _authService = AuthService();

  Future<Map<String, dynamic>> makeAuthenticatedRequest(
      String endpoint, Map<String, dynamic> data) async {
    String? token = await _authService.getCurrentUserToken();

    if (token == null) {
      throw Exception("User not authenticated. Please sign in.");
    }

    final response = await http.post(
      Uri.parse('$_baseUrl/$endpoint'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $token', // Crucial: Attach the ID token here
      },
      body: jsonEncode(data),
    );

    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      // Handle various error codes from your backend
      print('API Error: ${response.statusCode} - ${response.body}');
      throw Exception('Failed to process request: ${response.body}');
    }
  }
}

Server-Side (Conceptual)

On your custom backend (e.g., Node.js, Python, Go, Java), you’ll need to integrate the Firebase Admin SDK. This SDK provides the verifyIdToken method, which handles the complex cryptographic verification process.

Here’s a conceptual example using pseudo-code for a Node.js Express route, demonstrating the token verification:

// On your Node.js server (after initializing Firebase Admin SDK)
const admin = require('firebase-admin');

// --- Firebase Admin SDK Initialization (example, your setup might vary) ---
// You'll need to initialize the Admin SDK with your service account credentials.
// const serviceAccount = require('./path/to/your/serviceAccountKey.json');
// admin.initializeApp({
//   credential: admin.credential.cert(serviceAccount)
// });
// --------------------------------------------------------------------------

// Middleware to protect routes by verifying Firebase ID Token
async function authenticateFirebaseToken(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).send('Authorization header missing or malformed.');
  }

  const idToken = authHeader.split(' ')[1]; // Extract the token from "Bearer <token>"

  try {
    const decodedToken = await admin.auth().verifyIdToken(idToken);
    req.user = decodedToken; // Attach the decoded user info to the request object
    next(); // Proceed to the next middleware or route handler
  } catch (error) {
    console.error('Error verifying Firebase ID token:', error);
    // Token is invalid, expired, or other issues
    res.status(403).send('Invalid or expired authentication token.');
  }
}

// Example protected route in an Express app
app.post('/api/secure-profile-update', authenticateFirebaseToken, async (req, res) => {
  // If we reach here, req.user contains the decoded Firebase token payload
  const userId = req.user.uid; // The authenticated user's Firebase UID
  console.log(`Received secure request from user: ${userId}`);

  // Now, use 'userId' to interact with your custom database
  // e.g., update user profile data associated with this userId
  const { newEmail, newDisplayName } = req.body;
  // await myCustomDatabase.updateUserProfile(userId, { email: newEmail, displayName: newDisplayName });

  res.json({ message: 'Profile updated successfully!', userId: userId });
});

Handling Challenges & Common Mistakes

  • Token Expiration: Firebase ID tokens are short-lived (typically 1 hour). The getIdToken(true) call in Flutter’s AuthService automatically handles refreshing the token if it’s expired or about to expire, ensuring you always send a valid token to your backend.
  • Error Handling: Both Firebase Auth and your custom backend can throw errors. Implement robust try-catch blocks on the client-side to display meaningful messages to the user (e.g., “Invalid email or password,” “Network error”). Your backend should also return clear error codes and messages to help the client react appropriately.
  • Security:
    • Always use HTTPS for your custom backend API to encrypt all communication between your Flutter app and your server.
    • Never store Firebase ID tokens directly on the client if you can avoid it. Retrieving it via _auth.currentUser.getIdToken(true) on demand is generally safer and leverages Firebase’s SDK for token management.
    • Ensure your Firebase Admin SDK service account key is securely stored on your server and never exposed publicly in your client-side code or repositories.
  • User Logout: When a user signs out, ensure you call _auth.signOut() to clear the local Firebase session. This also effectively invalidates the token locally, preventing its further use.

Conclusion

Adopting a hybrid backend with Firebase Authentication and a custom server offers a compelling blend of advantages: the convenience, security, and ease of Firebase for user management, combined with the flexibility, control, and scalability of your own infrastructure for everything else. By understanding how to effectively pass and verify Firebase ID tokens, you can build powerful, secure, and customizable Flutter apps that fit your unique needs. Happy coding!

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Optimizing Flutter UI Performance: Best Practices for Date Formatting and Expensive Operations

Optimizing Flutter UI Performance: Best Practices for Date Formatting and Expensive Operations

Developers often face performance bottlenecks when performing expensive operations like date formatting directly within Flutter's `build` method, especially in fast-scrolling lists. This post will delve into common pitfalls, explain why these operations are costly, and provide practical strategies for optimizing UI performance by caching formatters, using `initState`, and leveraging `compute` for background processing without blocking the UI.

Cover image for Optimizing Your Flutter Dev Setup: IDEs, Simulators, and AI Tools for Peak Productivity

Optimizing Your Flutter Dev Setup: IDEs, Simulators, and AI Tools for Peak Productivity

Flutter developers frequently seek to refine their development environments. This post will dive into popular IDE choices like VS Code and Android Studio, discuss best practices for managing iOS and Android simulators (including in-IDE options), and explore the practical integration of AI tools for code generation and problem-solving to boost overall efficiency.

Cover image for Demystifying Flutter Performance: Practical Strategies for Large-Scale Apps

Demystifying Flutter Performance: Practical Strategies for Large-Scale Apps

Flutter's performance is often blamed for issues in complex applications, but the real culprits are usually architectural decisions, inefficient widget rebuilds, and unoptimized resource handling. This post will dive into common performance bottlenecks in large Flutter apps, providing actionable strategies for profiling, optimizing state management, handling images and network requests efficiently, and leveraging CI/CD for continuous performance monitoring.