← back to writing

Firebase in Flutter — Environments and Flavors Without Tears

18 March 20266 minFlutter · Firebase · DevOps

Every Flutter + Firebase app I've shipped has eventually needed three environments: a noisy dev where analytics events can be wrong, a staging mirror that QA owns, and prod where nothing is allowed to break. Getting there cleanly is mostly a tooling problem — here's the setup I reach for now.

Step 1 — three Firebase projects, three config files

Create three Firebase projects and run flutterfire configure against each with a distinct output path:

flutterfire configure \
  --project=my-app-dev \
  --out=lib/firebase_options_dev.dart \
  --ios-bundle-id=com.myapp.dev \
  --android-package-name=com.myapp.dev

Repeat for staging and prod. You now have three firebase_options_*.dart files checked in.

Step 2 — flavors with flutter_flavorizr

flutter_flavorizr handles the Android flavor dimensions and iOS schemes/configurations in one pass. A minimal flavorizr.yaml:

flavors:
  dev:
    app:
      name: "MyApp Dev"
    android:
      applicationId: "com.myapp.dev"
    ios:
      bundleId: "com.myapp.dev"
  prod:
    app:
      name: "MyApp"
    android:
      applicationId: "com.myapp"
    ios:
      bundleId: "com.myapp"

Run dart run flutter_flavorizr and commit the changes.

Step 3 — one entry point per flavor

// lib/main_dev.dart
Future<void> main() => bootstrap(
  options: DefaultFirebaseOptions.dev,
  flavor: Flavor.dev,
);

bootstrap() is a shared function that initializes Firebase, sets up Crashlytics, registers DI, and runs the app. The only thing each entry point changes is which options go in.

Step 4 — run it

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

The small things that bite


That's the whole shape of it. Once this is wired, adding a new environment is a ten-minute job instead of a branch-long detour.