Firebase Integration

FlutterFire CLI & Firebase Options

15 min Lesson 2 of 13

FlutterFire CLI & Firebase Options

Manually copying API keys, project IDs, and configuration values from the Firebase console into your Flutter app is error-prone and tedious. The FlutterFire CLI solves this problem by auto-generating a firebase_options.dart file that contains a typed FirebaseOptions object for every target platform. You simply point the CLI at your Firebase project and it does the heavy lifting.

Installing the FlutterFire CLI

The FlutterFire CLI is a Dart-based command-line tool published on pub.dev. Install it globally alongside the Firebase CLI:

Installing the CLI tools

# Install (or update) the Firebase CLI via npm
npm install -g firebase-tools

# Log in to Firebase
firebase login

# Install the FlutterFire CLI globally
dart pub global activate flutterfire_cli

# Verify the installation
flutterfire --version
Note: Make sure ~/.pub-cache/bin is in your PATH so the flutterfire command is available globally. On macOS/Linux, add export PATH="$PATH":"$HOME/.pub-cache/bin" to your shell profile (.zshrc or .bashrc).

Running flutterfire configure

Navigate to the root of your Flutter project (where pubspec.yaml lives) and run:

Generating firebase_options.dart

# Interactive mode — prompts you to select a Firebase project and platforms
flutterfire configure

# Non-interactive mode — specify project and platforms explicitly
flutterfire configure \
  --project=my-app-prod \
  --platforms=android,ios,web

The CLI will:

  • Fetch your Firebase project configuration from the Firebase API
  • Register your app for each selected platform (Android, iOS, Web, macOS) if not already registered
  • Download google-services.json (Android) and GoogleService-Info.plist (iOS) into the correct native directories
  • Generate lib/firebase_options.dart containing a DefaultFirebaseOptions class
Tip: Re-run flutterfire configure any time you add a new platform or change Firebase services. The CLI safely overwrites firebase_options.dart and the native files without touching your Dart business logic.

Anatomy of firebase_options.dart

The generated file exports a DefaultFirebaseOptions class with a static method currentPlatform. This method returns the correct FirebaseOptions object at runtime based on defaultTargetPlatform:

Generated firebase_options.dart (simplified)

// File generated by FlutterFire CLI — DO NOT edit manually.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
    show defaultTargetPlatform, kIsWeb, TargetPlatform;

class DefaultFirebaseOptions {
  static FirebaseOptions get currentPlatform {
    if (kIsWeb) {
      return web;
    }
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        return android;
      case TargetPlatform.iOS:
        return ios;
      case TargetPlatform.macOS:
        return macos;
      default:
        throw UnsupportedError(
          'DefaultFirebaseOptions are not supported for this platform.',
        );
    }
  }

  static const FirebaseOptions android = FirebaseOptions(
    apiKey: 'AIzaSy....',
    appId: '1:123456789:android:abcdef',
    messagingSenderId: '123456789',
    projectId: 'my-app-prod',
    storageBucket: 'my-app-prod.appspot.com',
  );

  static const FirebaseOptions ios = FirebaseOptions(
    apiKey: 'AIzaSy....',
    appId: '1:123456789:ios:abcdef',
    messagingSenderId: '123456789',
    projectId: 'my-app-prod',
    storageBucket: 'my-app-prod.appspot.com',
    iosClientId: 'com.example.myapp',
    iosBundleId: 'com.example.myapp',
  );

  static const FirebaseOptions web = FirebaseOptions(
    apiKey: 'AIzaSy....',
    appId: '1:123456789:web:abcdef',
    messagingSenderId: '123456789',
    projectId: 'my-app-prod',
    storageBucket: 'my-app-prod.appspot.com',
    authDomain: 'my-app-prod.firebaseapp.com',
    measurementId: 'G-XXXXXXXX',
  );
}

Initializing Firebase in main.dart

With firebase_options.dart generated, initializing Firebase in your app is a single call. Always await it inside an async main before running the widget tree:

Wiring Firebase.initializeApp() in main.dart

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';

Future<void> main() async {
  // Ensure Flutter engine is ready before calling native plugins
  WidgetsFlutterBinding.ensureInitialized();

  // Pass the platform-specific options from the generated file
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  runApp(const MyApp());
}
Warning: Never call Firebase.initializeApp() without await. Calling it synchronously returns a Future that is never resolved, so every subsequent Firebase call (Firestore, Auth, etc.) will throw a FirebaseException with the message "No Firebase App has been created."

Managing Dev and Prod Environments

A common production pattern is to maintain two separate Firebase projects — one for development and one for production — each with its own API keys, databases, and storage buckets. The FlutterFire CLI supports this via the --project flag combined with Dart build flavors:

Generating options for multiple environments

# Generate options for the dev project into a named file
flutterfire configure \
  --project=my-app-dev \
  --out=lib/firebase_options_dev.dart \
  --yes

# Generate options for the prod project into a separate file
flutterfire configure \
  --project=my-app-prod \
  --out=lib/firebase_options_prod.dart \
  --yes

Then select the correct options file at startup using a compile-time constant or a Dart flavor:

Switching Firebase project by flavor

// main_dev.dart  — entry point for the "dev" flavor
import 'firebase_options_dev.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp(environment: 'development'));
}

// main_prod.dart  — entry point for the "prod" flavor
import 'firebase_options_prod.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp(environment: 'production'));
}
Tip: Add lib/firebase_options*.dart to your .gitignore if your Firebase API keys are sensitive. For open-source projects, Firebase Web API keys are not secret (they identify the project, not authenticate it — Firebase Security Rules are your real access guard), but server keys (service account JSON) should never be committed.

Summary

The FlutterFire CLI is the standard, officially recommended way to connect a Flutter app to Firebase. Running flutterfire configure generates a strongly-typed DefaultFirebaseOptions class with per-platform configuration. Calling Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) in an async main() bootstraps every Firebase service before the widget tree runs. For multi-environment setups, generate separate options files for dev and prod and select the right one via Dart flavors or compile-time entry points.