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
- iOS
GoogleService-Info.plist: per-scheme copy build phase, not checked into the target directly - Crashlytics dSYMs: upload per flavor; the fastlane plugin handles this cleanly in CI
- App Check: use a debug provider in dev, Play Integrity / DeviceCheck in prod — App Check silently blocking Firestore reads in staging is a classic half-hour debugging session
- Analytics: always tag events with the flavor in a custom parameter — you will want this the first time a dashboard looks wrong
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.