How to Setup Flutter & Firebase with Multiple Flavors using the FlutterFire CLI

If your Flutter app supports multiple flavors and connects to Firebase, you need some extra setup to ensure each flavor corresponds to a different Firebase environment.

The best approach is to create a separate Firebase project for each flavor. This keeps your development, staging, and production environments separate.

Each Flutter flavor maps to a separate Firebase project

When using custom backends or Dart SDKs like Supabase, you can connect to the correct environment by switching the URL and API key based on the flavor. But Firebase does not offer a Dart SDK and requires some platform-specific setup, making the flavoring process more complex.

Thankfully, the FlutterFire CLI comes to the rescue. I'll walk you through using it to flavor your Flutter & Firebase apps without losing your mind. 😅

Here's what we will cover:

  • Why do we need FlutterFire?
  • Installing the Firebase and FlutterFire CLI
  • FlutterFire Config Syntax for Multiple Flavors
  • Easier Setup with a Shell Script
  • Initializing Firebase during App Startup (iOS, Android, and web)

By the end, you'll be able to confidently integrate Firebase into your multi-flavor Flutter app, saving time and avoiding common setup headaches.

Prerequisites

Note: This guide assumes you already have a Flutter app that runs correctly with dev, stg, and prod flavors on iOS and Android:

flutter run --flavor dev flutter run --flavor stg flutter run --flavor prod

You'll also need three Firebase projects, like in this example:

Three Firebase projects for the Flutter app

You can use Flutter Flavorizr to add flavors to your app. For more guidance, check out my Flutter in Production course.

Ready? Let's go! 🚀

Why do we need FlutterFire?

Adding Firebase to a Flutter app used to be a tedious process. You’d have to manually download configuration files for each platform (like GoogleService-Info.plist for iOS and google-services.json for Android).

Now, the process is much simpler. You just run flutterfire configure and follow some interactive prompts. Once finished, these files are automatically added to your project:

  • lib/firebase_options.dart
  • ios/Runner/GoogleService-Info.plist
  • android/app/google-services.json

However, when working with multiple flavors, it gets trickier. You’ll need separate versions of these files for each flavor, stored in different locations to avoid overwriting them during the configuration process.

Luckily, FlutterFire 1.0.0 added support for multiple flavors. Let's explore how to use it.

Installing the Firebase and FlutterFire CLI

The official docs cover all the steps for installing the Firebase and FlutterFire CLIs.

The Firebase CLI can be installed as a standalone library or via npm. I’ve found the npm approach to be the most reliable:

npm install -g firebase-tools

To check your installation, run: firebase --version.

Next, log in by running firebase login. You’ll see this prompt:

? Allow Firebase to collect CLI and Emulator Suite usage and error reporting information? Yes

This opens a browser window where you can sign in with Google. After selecting your account, you’ll see this:

Firebase CLI wants to access your Google Account

Click "Allow" and close the window. Now, the Firebase CLI is logged in.

Note: Make sure you use the Google account linked to the Firebase projects you want to work with. If you’re logged in with the wrong account, run firebase logout and firebase login again.

Installing the FlutterFire CLI

To install the FlutterFire CLI, run:

dart pub global activate flutterfire_cli

Then, check that you’re on version 1.0.0 or above by running flutterfire --version.

FlutterFire Config Syntax with Multiple Flavors

Let's consider this command, which generates all the config files for the dev flavor:

flutterfire config \ --project=flutter-ship-dev \ --out=lib/firebase_options_dev.dart \ --ios-bundle-id=com.codewithandrea.flutterShipApp.dev \ --ios-out=ios/flavors/dev/GoogleService-Info.plist \ --android-package-name=com.codewithandrea.flutter_ship_app.dev \ --android-out=android/app/src/dev/google-services.json

Here’s what each argument does:

  • --project: The Firebase project to use (note: pass the project ID, not the alias).
  • --out: Output path for the Firebase config file.
  • --ios-bundle-id: iOS app’s bundle ID. Find it in Xcode under Runner > General > Identity > Bundle Identifier.
  • --ios-out: Output path for the iOS GoogleService-Info.plist.
  • --android-package-name: Android app’s package name (found as applicationId in android/app/build.gradle).
  • --android-out: Output path for the Android google-services.json.

To learn about all the available options, run flutterfire config --help.

To use this command, you can:

  1. Copy it into your terminal.
  2. Update the project, ios-bundle-id, and android-package-name for your app.
  3. Run it and follow the interactive prompts (we’ll cover these in a moment).

But you’ll need to repeat this for the stg and prod flavors. That’s time-consuming and error prone.

Let's see if we can automate the process. 👇

Easier Setup with a Shell Script

While flutterfire config handles most of the work, you still need to run it for each flavor with different arguments.

To streamline this, create a flutterfire-config.sh script and save it at the root of your project:

#!/bin/bash # Script to generate Firebase configuration files for different environments/flavors # Feel free to reuse and adapt this script for your own projects if [[ $# -eq 0 ]]; then echo "Error: No environment specified. Use 'dev', 'stg', or 'prod'." exit 1 fi case $1 in dev) flutterfire config \ --project=flutter-ship-dev \ --out=lib/firebase_options_dev.dart \ --ios-bundle-id=com.codewithandrea.flutterShipApp.dev \ --ios-out=ios/flavors/dev/GoogleService-Info.plist \ --android-package-name=com.codewithandrea.flutter_ship_app.dev \ --android-out=android/app/src/dev/google-services.json ;; stg) flutterfire config \ --project=flutter-ship-stg \ --out=lib/firebase_options_stg.dart \ --ios-bundle-id=com.codewithandrea.flutterShipApp.stg \ --ios-out=ios/flavors/stg/GoogleService-Info.plist \ --android-package-name=com.codewithandrea.flutter_ship_app.stg \ --android-out=android/app/src/stg/google-services.json ;; prod) flutterfire config \ --project=flutter-ship-prod \ --out=lib/firebase_options_prod.dart \ --ios-bundle-id=com.codewithandrea.flutterShipApp \ --ios-out=ios/flavors/prod/GoogleService-Info.plist \ --android-package-name=com.codewithandrea.flutter_ship_app \ --android-out=android/app/src/prod/google-services.json ;; *) echo "Error: Invalid environment specified. Use 'dev', 'stg', or 'prod'." exit 1 ;; esac

With this script, you still need to set the correct arguments for your project, but you only need to do this once.

Then, generating all the Firebase config files becomes a breeze—no need to remember each argument.

Time to take take the script for a ride. 👇

Running the FlutterFire Script for each Flavor

To configure the dev flavor, run:

./flutterfire-config.sh dev

When prompted, select "Build configuration":

? You have to choose a configuration type. Either build configuration (most likely choice) or a target set up. â€ș ❯ Build configuration Target

Then, choose the Debug-dev build configuration:

? Please choose one of the following build configurations â€ș Debug Release Profile ❯ Debug-dev Profile-dev Release-dev Debug-stg Profile-stg Release-stg Debug-prod Profile-prod Release-prod

Note: If you encounter a "Failed to list Firebase projects" error, run firebase logout, then firebase login, and try again.

Next, choose the platforms you want to configure:

? Which platforms should your configuration support (use arrow keys & space to select)? â€ș ✔ android ✔ ios macos ✔ web windows

This step may take some time as the CLI registers the necessary apps with Firebase. If successful, you’ll see a confirmation similar to this:

✔ You have to choose a configuration type. Either build configuration (most likely choice) or a target set up. · Build configuration ✔ Please choose one of the following build configurations · Debug-dev i Found 40 Firebase projects. Selecting project flutter-ship-dev. ✔ Which platforms should your configuration support (use arrow keys & space to select)? · android, ios, web i Firebase android app com.codewithandrea.flutter_ship_app.dev is not registered on Firebase project flutter-ship-dev. i Registered a new Firebase android app on Firebase project flutter-ship-dev. i Firebase ios app com.codewithandrea.flutterShipApp.dev is not registered on Firebase project flutter-ship-dev. i Registered a new Firebase ios app on Firebase project flutter-ship-dev. i Firebase web app flutter_ship_app (web) is not registered on Firebase project flutter-ship-dev. i Registered a new Firebase web app on Firebase project flutter-ship-dev. Firebase configuration file lib/firebase_options_dev.dart generated successfully with the following Firebase apps: Platform Firebase App Id web 1:424176442589:web:c86e231d1eeaba0e90cf34 android 1:424176442589:android:c5841ba53606b4c490cf34 ios 1:424176442589:ios:592b56a800affa4e90cf34 Learn more about using this file and next steps from the documentation: > https://firebase.google.com/docs/flutter/setup

Next, repeat the same steps for the stg flavor by running:

./flutterfire-config.sh stg

And again for the prod flavor:

./flutterfire-config.sh prod

Once complete, your project will have these new files:

lib/firebase_options_dev.dart lib/firebase_options_stg.dart lib/firebase_options_prod.dart ios/flavors/dev/GoogleService-Info.plist ios/flavors/stg/GoogleService-Info.plist ios/flavors/prod/GoogleService-Info.plist android/app/src/dev/google-services.json android/app/src/stg/google-services.json android/app/src/prod/google-services.json

Should the Firebase config files be added to Git?

The files above don’t contain sensitive information, so it’s safe to commit them to Git.

However, in my open-source projects, I prefer to add them to .gitignore:

# Ignore Firebase configuration files lib/firebase_options*.dart ios/Runner/GoogleService-Info.plist ios/flavors/*/GoogleService-Info.plist macos/Runner/GoogleService-Info.plist macos/flavors/*/GoogleService-Info.plist android/app/google-services.json android/app/src/*/google-services.json

This has two implications:

  • For a fresh checkout, you’ll need to run flutterfire-config.sh again for each flavor.
  • On CI, you can store these files as environment secrets and add a pre-build step to restore them to their correct locations.

FlutterFire setup complete ✅

If you followed all the steps above without errors, all the Firebase configuration files should now be in your project.

Before running the app with Firebase, there are a few more steps to tackle:

  • Install the firebase_core package and verify the app runs on Android and iOS
  • Initialize Firebase when the app starts

Let’s walk through them. 👇

Installing the firebase_core package

To add firebase_core, run this in your terminal:

flutter pub add firebase_core flutter pub get

Running the app on Android

If you try to run the Android app, you may see an error about the "com.google.gms.google-services" plugin not being found.

To fix this, open android/build.gradle and add a buildscript section at the top:

buildscript { repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.3.0' classpath 'com.google.gms:google-services:4.4.2' } }

How to Choose the Correct Versions?

  • Find the Gradle version in gradle-wrapper.properties.
  • Check the latest version of google-services in Google's Maven Repository.

After applying this fix, the Android app should run correctly.

Running the app on iOS

Before running the iOS app, open ios/Podfile and ensure the platform version is set to 13.0 or higher:

# Uncomment this line to define a global platform for your project platform :ios, '13.0'

Run pod install, and you should be able to run the app on iOS.

Firebase Initialization During App Startup

According to the official docs, you should add the Firebase initialization code to lib/main.dart:

import 'package:flutter_ship_app/firebase_options.dart'; // inside main() await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, );

However, this default setup won’t work for us because we have separate configuration files for each flavor:

The Firebase options files

So, how can we handle this?

Option 1: Centralize the Firebase Initialization logic

One option is to create a firebase.dart file with the following code:

// firebase.dart import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_ship_app/firebase_options_prod.dart' as prod; import 'package:flutter_ship_app/firebase_options_stg.dart' as stg; import 'package:flutter_ship_app/firebase_options_dev.dart' as dev; Future<void> initializeFirebaseApp() async { // Determine which Firebase options to use based on the flavor final firebaseOptions = switch (appFlavor) { 'prod' => prod.DefaultFirebaseOptions.currentPlatform, 'stg' => stg.DefaultFirebaseOptions.currentPlatform, 'dev' => dev.DefaultFirebaseOptions.currentPlatform, _ => throw UnsupportedError('Invalid flavor: $flavor'), }; await Firebase.initializeApp(options: firebaseOptions); }

This works by switching on the appFlavor constant to return the correct FirebaseOptions object based on the flavor.

Note: When running on Flutter web with the --flavor option, you'll get a warning you that flavors are not fully supported. But the appFlavor constant will still return the correct value.

Now, you can simply call await initializeFirebaseApp() in lib/main.dart, which remains the single entry point for the app:

import 'firebase.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await initializeFirebaseApp(); runApp(const MainApp()); }

By default, Flutter Flavorizr generates separate entry points like main_dev.dart, main_stg.dart, and main_prod.dart. This can lead to unwanted code duplication, so I prefer using a single main.dart, as shown above, with switch expressions to handle flavor-specific initialization (there's a caveat though—more on this below).

With this setup, the Flutter app will initialize and connect to the correct Firebase project, depending on the flavor.

However, there's one issue. 👇

All the Firebase Config Files are Bundled (No Tree Shaking)

If you look closely at firebase.dart, you’ll notice that although the correct Firebase config is selected based on the flavor, all three firebase_options_*.dart files are still imported:

// firebase.dart import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; // Note: all three files are imported import 'package:flutter_ship_app/firebase_options_prod.dart' as prod; import 'package:flutter_ship_app/firebase_options_stg.dart' as stg; import 'package:flutter_ship_app/firebase_options_dev.dart' as dev; Future<void> initializeFirebaseApp() async { // Determine which Firebase options to use based on the flavor final firebaseOptions = switch (appFlavor) { 'prod' => prod.DefaultFirebaseOptions.currentPlatform, 'stg' => stg.DefaultFirebaseOptions.currentPlatform, 'dev' => dev.DefaultFirebaseOptions.currentPlatform, _ => throw UnsupportedError('Invalid flavor: $flavor'), }; await Firebase.initializeApp(options: firebaseOptions); }

This means that all three files are compiled and bundled during the build process because tree-shaking doesn't work here (the switch happens at runtime).

In theory, this could expose your development or staging environment details (which may be less secure than production) if someone reverse engineers your app.

While this might not be a big issue for apps that don’t handle sensitive data, it’s still a potential risk. If you want to mitigate this entirely, consider a more secure approach. 👇

Option 2: Use Multiple Entry Points

As we've seen, this code can be problematic:

import 'package:flutter_ship_app/firebase_options_prod.dart' as prod; import 'package:flutter_ship_app/firebase_options_stg.dart' as stg; import 'package:flutter_ship_app/firebase_options_dev.dart' as dev;

A more secure approach is to create three separate entry points—main_dev.dart, main_stg.dart, and main_prod.dart—which look like this:

// main_dev.dart import 'package:flutter_ship_app/firebase_options_dev.dart'; import 'main.dart'; void main() async { runMainApp(DefaultFirebaseOptions.currentPlatform); }

These files should do one thing only: import the correct firebase_options_*.dart file and pass the config as an argument to a function inside main.dart that performs the actual initialization. Here’s an example:

// main.dart import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; void runMainApp(FirebaseOptions firebaseOptions) async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: firebaseOptions); runApp(const MainApp()); }

This approach ensures that only the required Firebase configuration file is bundled, making it a secure and efficient solution for managing multiple flavors.

But how do you run the app with the right flavor? 👇

Running the App with a Specific Flavor

If you use the second option outlined above, you'll have four files:

  • main_dev.dart: Entry point for dev
  • main_stg.dart: Entry point for stg
  • main_prod.dart: Entry point for prod
  • main.dart: Contains the app initialization code

As a result, you can run the app with a specific flavor using these commands:

flutter run --flavor dev -t lib/main_dev.dart flutter run --flavor stg -t lib/main_stg.dart flutter run --flavor prod -t lib/main_prod.dart

This way, the correct entry point is used for each flavor, ensuring that the right Firebase environment is connected when the app launches.

If you follow this approach, make sure to update your local configuration (e.g., .vscode/launch.json) and CI/CD scripts to reflect these changes.

Which Option Should you Choose?

Both options have their pros and cons, and the right choice depends on your project’s needs:

  • Option 1: Centralized Firebase Initialization. This approach is easier and quicker to implement. It allows you to use a single main.dart file and handle flavor-specific Firebase options dynamically at runtime. However, because all Firebase configuration files are bundled in the final app (even if they’re not used), it's not the best choice for security reasons.
  • Option 2: Multiple Entry Points for Each Flavor. This option requires a bit more setup because you’ll need to create separate entry points for each flavor (main_dev.dart, main_stg.dart, main_prod.dart). However, it only bundles the necessary Firebase configuration file for each build, making it a more secure solution, since an attacker won’t have access to the environment details of other flavors.

While option 2 takes a bit more work, I recommend it for multi-flavor Flutter apps that use Firebase. 👍

Option 1 works just fine for non-Firebase apps, since you can use --dart-define-from-file and define environment variables inside separate files (e.g. .env.dev, .env.stg, .env.prod) for each flavor. To learn more, read: How to Store API Keys in Flutter: --dart-define vs .env files.

Conclusion

By leveraging FlutterFire alongside a simple shell script, we’ve streamlined what used to be a complex and error-prone flavoring process. Instead of manually configuring Firebase for each flavor, you can now generate the necessary files for all environments with a single script, saving time and reducing the chance of mistakes.

The centralized Firebase initialization option offers a quick and simple way to connect your app to the correct Firebase project at startup, using a single main.dart file and flavor-specific logic. However, this approach bundles all Firebase configurations, which may not be ideal for security-sensitive apps.

For more secure setups, the multiple entry points strategy ensures that only the necessary Firebase configuration is included in each build, making it a better choice when handling sensitive data or production-grade apps.

With either approach, your Flutter app will automatically connect to the appropriate Firebase environment—whether that’s dev, stg, or prod—ensuring your app behaves as expected in every stage of development and production.

Each Flutter flavor maps to a separate Firebase project

This setup makes it easier to manage multiple flavors in your Flutter & Firebase apps, and I've been happily using it in production for my own apps. ✅

New Course: Flutter in Production

When it comes to shipping and maintaining apps in production, there are many important aspects to consider:

  • Preparing for release: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&Cs
  • App Submissions: app store metadata & screenshots, compliance, testing vs distribution tracks, dealing with rejections
  • Release automation: CI workflows, environment variables, custom build steps, code signing, uploading to the stores
  • Post-release: error monitoring, bug fixes, addressing user feedback, adding new features, over-the-air updates

My latest course will help you get your app to the stores faster and with fewer headaches.

If you’re interested, you can learn more and enroll here (currently 40% off!). 👇

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter Foundations Course

Flutter Foundations Course

Learn about State Management, App Architecture, Navigation, Testing, and much more by building a Flutter eCommerce app on iOS, Android, and web.

Flutter & Firebase Masterclass

Flutter & Firebase Masterclass

Learn about Firebase Auth, Cloud Firestore, Cloud Functions, Stripe payments, and much more by building a full-stack eCommerce app with Flutter & Firebase.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Last updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.