An app that uses Firebase should isolate development activity from production data.
A practical default architecture is to use separate Firebase projects and bind each Android product flavor to one fixed environment.
Recommended structure
| Environment | applicationId | Firebase project_id | App name |
|---|---|---|---|
| Development | com.example.app.dev | example-app-dev | Example Dev |
| Production | com.example.app | example-app-prod | Example |
Because the package names differ, both apps can be installed on the same device at the same time.
Why separate Firebase projects?
Registering both Android apps in one Firebase project does not isolate their backend data. Firestore, Authentication, Storage, and other products still belong to that shared project.
Use separate Firebase projects when development data must not affect production.
This separates resources such as:
- Authentication users
- Firestore data
- Realtime Database
- Cloud Storage
- Cloud Functions
- Security Rules
- Remote Config
- Cloud Messaging
- Crashlytics
- Analytics
- App Check
Step 1: Create clearly named Firebase projects
Example:
example-app-dev
example-app-prod
Avoid ambiguous names such as example-app and example-app-2. The environment should be obvious in the Firebase console, Google Cloud console, CLI, and logs.
Step 2: Register the Android apps
Development project:
com.example.app.dev
Production project:
com.example.app
Download the corresponding google-services.json from each Firebase project.
Step 3: Configure Android product flavors
Kotlin DSL example:
android {
namespace = "com.example.app"
defaultConfig {
applicationId = "com.example.app"
}
flavorDimensions += "environment"
productFlavors {
create("development") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
resValue("string", "app_name", "Example Dev")
}
create("production") {
dimension = "environment"
resValue("string", "app_name", "Example")
}
}
}
This creates variants such as:
developmentDebug
developmentRelease
productionDebug
productionRelease
Step 4: Place the JSON file by flavor
app/src/development/google-services.json
app/src/production/google-services.json
Development configuration:
{
"project_info": {
"project_id": "example-app-dev"
}
}
Production configuration:
{
"project_info": {
"project_id": "example-app-prod"
}
}
Avoid keeping only the production file at app/google-services.json in a flavor-based setup. Explicit flavor directories make missing or incorrect environment configuration easier to detect.
Step 5: Make development builds visually distinct
Reduce the risk of using the wrong app or environment.
Common safeguards include:
- Add “Dev” to the development app name
- Add a DEV badge to the development icon
- Show the active environment inside debug builds
- Enable internal debugging tools only in development
The production app does not need a permanent environment label.
Log the active Firebase project
Android:
val app = FirebaseApp.getInstance()
Log.d("FirebaseEnv", "project=${app.options.projectId}")
Flutter:
final app = Firebase.app();
debugPrint('project=${app.options.projectId}');
Expected values:
development → example-app-dev
production → example-app-prod
Do not log secrets or user data while checking the environment.
Separate environments in Flutter
Example structure:
lib/
├── bootstrap.dart
├── main_development.dart
├── main_production.dart
├── firebase_options_development.dart
└── firebase_options_production.dart
android/app/src/
├── development/google-services.json
└── production/google-services.json
Generate development options:
flutterfire configure \
--project=example-app-dev \
--android-package-name=com.example.app.dev \
--out=lib/firebase_options_development.dart
Generate production options:
flutterfire configure \
--project=example-app-prod \
--android-package-name=com.example.app \
--out=lib/firebase_options_production.dart
Shared bootstrap
Future<void> bootstrap({
required FirebaseOptions options,
}) async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: options);
runApp(const MyApp());
}
Run development
flutter run \
--flavor development \
--target lib/main_development.dart
Build the production AAB
flutter build appbundle \
--flavor production \
--target lib/main_production.dart
Ensure each entry point passes the matching generated FirebaseOptions to the shared bootstrap.
Separate Firebase CLI targets
Add project aliases:
firebase use --add
Example .firebaserc:
{
"projects": {
"dev": "example-app-dev",
"prod": "example-app-prod"
}
}
Specify the target during deployment:
firebase deploy --project=dev
firebase deploy --project=prod
For production deployments, do not rely only on whichever project is currently active in a developer’s local CLI session.
Validate the environment in CI
Before a production build, verify at least:
- The selected flavor is
production - The final
applicationIdiscom.example.app - The JSON
project_idisexample-app-prod - Production signing is active
- Production API and webhook endpoints are selected
Example JSON check:
python - <<'PY'
import json
from pathlib import Path
path = Path('android/app/src/production/google-services.json')
data = json.loads(path.read_text())
actual = data['project_info']['project_id']
expected = 'example-app-prod'
if actual != expected:
raise SystemExit(f'Wrong Firebase project: {actual}')
print(actual)
PY
Failing the build is safer than publishing an app connected to the wrong backend.
Configure Google Sign-In and SHA by environment
Development:
- Package:
com.example.app.dev - Debug SHA-1 and SHA-256
Production:
- Package:
com.example.app - Google Play app-signing SHA-1 and SHA-256
Register each fingerprint on the correct Android app inside the correct Firebase project.
Manage Security Rules separately
Do not accidentally deploy permissive development rules to production.
Production reviews should include:
- Authentication requirements
- Ownership checks
- Data-shape validation
- Roles and permissions
- Storage paths
- App Check
- Required indexes
Keeping configuration files private is not a substitute for secure backend rules.
When to use the Local Emulator Suite
A useful separation is:
Local unit and integration testing
→ Firebase Local Emulator Suite
Shared device and team testing
→ Development Firebase project
Google Play production app
→ Production Firebase project
Use emulators for workflows that do not need the real production cloud environment.
Practices to avoid
- Switching development and production through a runtime toggle
- Sharing one production JSON from the app-module root
- Switching only Dart options while leaving native configuration shared
- Giving development and production identical names and icons
- Trusting only the Firebase CLI active-project state
- Copying permissive development Security Rules into production
Summary
A safe environment boundary includes all of the following:
product flavor
applicationId
Firebase project_id
google-services.json
FirebaseOptions
signing and SHA configuration
CLI deployment target
Automatically checking the flavor, final application ID, and Firebase project ID before a production build significantly reduces accidental cross-environment connections.
For configuration-file details, see What Is google-services.json?.
Primary sources
Official references
Check the linked official documentation before a production release.
Continue reading
Related guides
TestCrew
Find testers through mutual testing
Test other Android apps, provide useful feedback, and use earned credits to recruit testers for your own Google Play closed test.
Learn how TestCrew works