Maintaining different environments for development, staging, and production is essential in any large-scale mobile app. In this post, I’ll walk you through how I implemented multi environment (flavor) support in a React Native app built with Expo + EAS — covering both Android and iOS — using separate configurations, build schemes, and runtime environment handling.
️ Overview
We wanted three independent variants of our app:
- Development (for internal testing)
- Staging (for UAT and QA)
- Production (live on the stores)
Each variant had its own:
- Bundle IDs / Package Names
- App names
- App icons (optional)
- Deep links
- Environment-specific variables (API endpoints, tokens, etc.)
⚙️ Project Structure
We used the following files to organize the environment-based configuration:
app.config.ts: Dynamic Expo config per environmenteas.json: EAS build profiles for each flavorgradlefiles: Product flavors and build variants on Android- Xcode targets +
.xcconfigfiles for iOS .envsupport (if needed, though we kept it insideapp.config.tsviaprocess.env)
Dynamic app.config.ts
We used a custom app.config.ts that reads an ENVIRONMENT variable and configures Expo accordingly.
import { ExpoConfig, ConfigContext } from 'expo/config';
export default ({ config }: ConfigContext): ExpoConfig => {
const env = process.env.ENVIRONMENT || 'development';
const flavorSettings = {
development: {
name: 'App Dev',
slug: 'app-dev',
androidPackage: 'com.example.dev',
iosBundleId: 'com.example.dev',
},
staging: {
name: 'App Staging',
slug: 'app-staging',
androidPackage: 'com.example.staging',
iosBundleId: 'com.example.staging',
},
production: {
name: 'App',
slug: 'app',
androidPackage: 'com.example',
iosBundleId: 'com.example',
},
};
const flavor = flavorSettings[env];
return {
...config,
name: flavor.name,
slug: flavor.slug,
android: {
package: flavor.androidPackage,
},
ios: {
bundleIdentifier: flavor.iosBundleId,
},
extra: {
ENVIRONMENT: env,
},
};
};
EAS Build Profiles (eas.json)
We then set up three build profiles in eas.json, each passing a custom ENVIRONMENT and custom gradle commands / Xcode schemes:
{
"build": {
"development": {
"extends": "base",
"env": {
"ENVIRONMENT": "development"
},
"android": {
"gradleCommand": ":app:bundleDevelopmentRelease"
},
"ios": {
"scheme": "app-dev"
}
},
"staging": {
"extends": "base",
"env": {
"ENVIRONMENT": "staging"
},
"android": {
"gradleCommand": ":app:bundleStagingRelease"
},
"ios": {
"scheme": "app-staging"
}
},
"production": {
"extends": "base",
"env": {
"ENVIRONMENT": "production"
},
"android": {
"gradleCommand": ":app:bundleProductionRelease"
},
"ios": {
"scheme": "app"
}
}
}
}
You can also include environment-specific variables like base URLs, API keys, or toggles in the env block.
Android: Product Flavors
We used product flavors in android/app/build.gradle:
flavorDimensions "default"
productFlavors {
development {
dimension "default"
applicationId "com.example.dev"
versionNameSuffix "-dev"
resValue "string", "app_name", "App Dev"
}
staging {
dimension "default"
applicationId "com.example.staging"
versionNameSuffix "-staging"
resValue "string", "app_name", "App Staging"
}
production {
dimension "default"
applicationId "com.example"
resValue "string", "app_name", "App"
}
}
Each flavor has its own package name, app name, and can have separate resources like strings.xml.
Android: Manifest-Specific Configurations
To support deep linking and intent filters specific to each flavor, we used different AndroidManifest.xml files per flavor.
Create these under:
android/app/src/development/AndroidManifest.xml android/app/src/staging/AndroidManifest.xml android/app/src/production/AndroidManifest.xml
In each manifest file, configure your deep links or intent filters like so:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application> <activity android:name=".MainActivity"> <!-- Deep link intent specific to this flavor --> <intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Replace with scheme relevant to the flavor --> <data android:scheme="yourapp.staging" /> </intent-filter> <!-- Optional: If your app reacts to download completion --> <intent-filter> <action android:name="android.intent.action.DOWNLOAD_COMPLETE" /> </intent-filter> </activity> </application> </manifest>
This allows each flavor to respond to different schemes or hostnames.
iOS: Multiple Targets + .xcconfig
To replicate Android-style flavoring on iOS, we created multiple targets in Xcode — one per environment (Development, Staging, Production). Each target allows unique bundle identifiers, app schemes, icons, and configs.
How to Create Targets in Xcode
- Open your project in Xcode (
.xcworkspace). - In the project navigator, right-click your main app target → Duplicate.
- Rename the duplicate to match the environment, like
MyAppDev,MyAppStaging. - Select the new target and:
- Update its Bundle Identifier in
Build Settings → Packaging. - Assign a custom Display Name in its
Info.plistif needed.
5. For each new target:
- Create
.xcconfigfiles likeDev.debug.xcconfig,Dev.release.xcconfig, etc. - Link them via
Build Settings → Configuration.
For each target:
- Defined unique Bundle ID
- Assigned unique
APP_SCHEME - Linked separate
.xcconfigfiles (for Debug and Release)
Info.plist, we referenced a build-time variable:<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>$(APP_SCHEME)</string> </array> </dict> </array>
In Xcode → Build Settings (per target), we added:
APP_SCHEME = app.dev APP_SCHEME = app.staging APP_SCHEME = app
This way, you don’t need different Info.plist files — just plug in the correct runtime values.
Scripts in package.json
We added custom scripts to streamline build and run tasks:
"android:dev": "ENVIRONMENT=development expo run:android --variant developmentDebug",
"android:staging": "ENVIRONMENT=staging expo run:android --variant stagingDebug",
"android:prod": "ENVIRONMENT=production expo run:android --variant productionDebug",
"ios:dev": "ENVIRONMENT=development expo run:ios --scheme app-dev",
"ios:staging": "ENVIRONMENT=staging expo run:ios --scheme app-staging",
"ios:prod": "ENVIRONMENT=production expo run:ios --scheme app",
You can run:
npm run android:staging npm run ios:prod
Triggering Builds and Submissions via Scripts
Once your eas.json, app.config.ts, native Android/iOS configurations, and environment variables are set up, you can leverage powerful build and submit scripts directly from your package.json.
These scripts make it super easy to automate builds and uploads for each environment without manual intervention.
️ Build Commands
"build:dev:android": "eas build --platform android --profile development",
"build:staging:android": "eas build --platform android --profile staging",
"build:prod:android": "eas build --platform android --profile production",
"build:dev:ios": "eas build --platform ios --profile development",
"build:staging:ios": "eas build --platform ios --profile staging",
"build:prod:ios": "eas build --platform ios --profile production"
Submit Commands
"submit:dev:android": "eas submit --platform android --profile development",
"submit:staging:android": "eas submit --platform android --profile staging",
"submit:prod:android": "eas submit --platform android --profile production",
"submit:dev:ios": "eas submit --platform ios --profile development",
"submit:staging:ios": "eas submit --platform ios --profile staging",
"submit:prod:ios": "eas submit --platform ios --profile production"
You can run these commands like so:
npm run build:staging:android npm run submit:prod:ios
This setup streamlines your deployment workflow and ensures consistency across environments.
Final Thoughts
With this setup:
- You isolate environments without code duplication.
- Each flavor gets its own deep links, configurations, and runtime settings.
- You gain full control over the build and release lifecycle for each variant.
This setup has significantly streamlined our CI/CD, testing, and release pipelines — making the app more maintainable and scalable.
✅ Quick Tips
- ❗ Always keep a single
Info.plistand use build variables. - Place variant-specific AndroidManifests inside respective
src/{flavor}folders. - ️ Use
.xcconfigfiles to manage per-target variables on iOS. - Use EAS Build’s
gradleCommandandschemeper profile for complete separation.
End to End Technology Solutions