Multi-Environment Setup for React Native CLI
A production-ready guide to configuring Dev, Staging, and Production environments in React Native — with Android product flavors, iOS schemes, and react-native-config.
Faisal Khawaj
Author
Switching between development, staging, and production backends should not require hand-editing .env files and rebuilding from scratch. This guide walks through a battle-tested setup for React Native CLI projects: separate environment files, native build variants, and yarn scripts that keep Metro and native builds in sync.
What you'll build
| Environment | Env file | App display name | Android flavor | iOS scheme |
|---|---|---|---|---|
| Development | .env | MyApp Dev | dev | App-Dev |
| Staging | .env.staging | MyApp Staging | staging | App-Staging |
| Production | .env.production | MyApp | prod | App-Prod |
Each environment gets its own API URLs, feature flags, and home-screen label — from a single codebase.
The problem this solves
Without flavors or schemes, every environment switch means:
- Manually editing
.env - Rebuilding the native app
- Hoping Metro picked up the same values
With this setup you can:
- Point each build at a different backend (dev / staging / prod)
- Show a distinct app name on the home screen (e.g. "MyApp Staging")
- Ship one codebase with separate store release configurations
- Run QA and production builds side by side (with optional bundle ID suffixes)
Architecture overview
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ .env file │────▶│ react-native- │────▶│ src/config/ │
│ (per env) │ │ config │ │ env.ts │
└─────────────┘ └──────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────────┐
│ Metro │ │ Native build │
│ (JS bundle) │ │ (Android/iOS) │
└─────────────┘ └──────────────────┘Golden rule: Metro and the native app must load the same environment file.
| ✅ Correct | ❌ Wrong |
|---|---|
start:staging + android:staging | start:dev + android:staging |
Quick start
1. Create environment files
Add these to your project root. They are gitignored — never commit secrets.
| File | APP_ENV value | Used for |
|---|---|---|
.env | development | Local development |
.env.staging | staging | QA / staging server |
.env.production | production | App Store / Play Store |
Example .env:
APP_ENV=development
API_URL=https://api-dev.example.com/
SOCKET_URL=https://api-dev.example.com/chat
# Analytics, payments, etc.
SENTRY_DSN=
POSTHOG_API_KEY=Important: Environment values are baked in at build time, not read live. After changing any
.envfile, rebuild the native app.
2. Run with matching scripts
Open two terminals and pick one environment:
| Environment | Terminal 1 (Metro) | Terminal 2 (run app) |
|---|---|---|
| Dev | yarn start:dev | yarn android:dev or yarn ios:dev |
| Staging | yarn start:staging | yarn android:staging or yarn ios:staging |
| Production | yarn start:prod | yarn android:prod or yarn ios:prod |
Building blocks
| Piece | Platform | Responsibility |
|---|---|---|
.env files | Both | API URLs, keys, feature flags |
react-native-config | Both | Injects env vars into native + JS at build time |
| Product flavors | Android | dev, staging, prod build variants |
| Xcode schemes | iOS | App-Dev, App-Staging, App-Prod |
| Yarn scripts | Both | Wire Metro + native build to the right env |
Read values in code
Prefer typed helpers in src/config/env.ts:
import { getApiBaseUrl, getAppEnv } from '@/config/env';
const baseUrl = getApiBaseUrl();
const env = getAppEnv(); // 'development' | 'staging' | 'production'Add new keys to src/types/react-native-config.d.ts for TypeScript support.
Android setup
Android uses product flavors in android/app/build.gradle.
Step 1 — Map variants to env files
Add before dotenv.gradle is applied:
project.ext.envConfigFiles = [
devDebug: ".env",
devRelease: ".env",
stagingDebug: ".env.staging",
stagingRelease: ".env.staging",
prodDebug: ".env.production",
prodRelease: ".env.production",
]
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"Step 2 — Define flavors
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
versionNameSuffix "-dev"
resValue "string", "app_name", "MyApp Dev"
}
staging {
dimension "env"
versionNameSuffix "-staging"
resValue "string", "app_name", "MyApp Staging"
}
prod {
dimension "env"
resValue "string", "app_name", "MyApp"
}
}app_name is referenced by AndroidManifest.xml via android:label="@string/app_name".
Step 3 — Enable Metro for debug flavors
Inside the react { } block:
debuggableVariants = ["devDebug", "stagingDebug", "prodDebug"]Skip this and Metro won't attach to flavored debug builds.
Step 4 — Add run scripts
"android:dev": "ENVFILE=.env react-native run-android --mode=devDebug",
"android:staging": "ENVFILE=.env.staging react-native run-android --mode=stagingDebug",
"android:prod": "ENVFILE=.env.production react-native run-android --mode=prodDebug"Variant names are camelCase: devDebug, not dev-debug.
Android build variants
A variant = flavor + build type:
flavor + build type = variant
dev + debug = devDebug
staging + debug = stagingDebug
prod + release = prodRelease ← Play StoreStore releases
yarn android:prod:release # APK → android/app/build/outputs/apk/prod/release/
yarn android:prod:aab # AAB → android/app/build/outputs/bundle/prodRelease/iOS setup
iOS uses Xcode schemes. Each scheme runs a script before build to pick the right .env file.
Step 1 — xcconfig files
Create ios/Config/Debug.xcconfig and Release.xcconfig:
#include "../Pods/Target Support Files/Pods-<YourApp>/Pods-<YourApp>.debug.xcconfig"
#include? "../tmp.xcconfig"tmp.xcconfig is generated at build time — do not commit it. Point your app target's Debug / Release configurations to these files in Xcode.
Step 2 — Environment script
ios/scripts/set-env-file.sh runs before each build. It:
- Tells
react-native-configwhich.envfile to use - Generates
ios/tmp.xcconfigwith env variables - Sets the app display name on the home screen
Step 3 — Create one scheme per environment
Xcode → Product → Scheme → Edit Scheme → Build → Pre-actions → +
| Scheme | Pre-action command |
|---|---|
| App-Dev | "${SRCROOT}/scripts/set-env-file.sh" ".env" |
| App-Staging | "${SRCROOT}/scripts/set-env-file.sh" ".env.staging" |
| App-Prod | "${SRCROOT}/scripts/set-env-file.sh" ".env.production" |
Check "Provide build settings from" → select your app target. Save schemes under xcshareddata/xcschemes/ so they are committed to git.
Step 4 — Add run scripts
"ios:dev": "ENVFILE=.env react-native run-ios --scheme App-Dev",
"ios:staging": "ENVFILE=.env.staging react-native run-ios --scheme App-Staging",
"ios:prod": "ENVFILE=.env.production react-native run-ios --scheme App-Prod --mode Release"App Store / TestFlight
- Open
ios/<YourApp>.xcworkspacein Xcode - Select the production scheme
- Product → Archive
Complete script reference
| Script | Loads | Android variant | iOS scheme |
|---|---|---|---|
yarn start:dev | .env | — | — |
yarn android:dev | .env | devDebug | — |
yarn ios:dev | .env | — | App-Dev |
yarn start:staging | .env.staging | — | — |
yarn android:staging | .env.staging | stagingDebug | — |
yarn ios:staging | .env.staging | — | App-Staging |
yarn start:prod | .env.production | — | — |
yarn android:prod | .env.production | prodDebug | — |
yarn ios:prod | .env.production | — | App-Prod |
yarn android:prod:release | .env.production | prodRelease (APK) | — |
yarn android:prod:aab | .env.production | prodRelease (AAB) | — |
android:prodandios:prodare for testing production config locally — not for store submission.
Adding a new environment (e.g. QA)
| Step | Android | iOS |
|---|---|---|
| 1 | Create .env.qa | Same file |
| 2 | Add qaDebug / qaRelease to envConfigFiles | Duplicate a scheme → App-QA |
| 3 | Add qa flavor in productFlavors | Pre-action: set-env-file.sh ".env.qa" |
| 4 | Add qaDebug to debuggableVariants | Add display name in set-env-file.sh |
| 5 | Add start:qa, android:qa, ios:qa scripts | — |
Install multiple environments side by side
By default, all flavors share one bundle ID — only one can be installed at a time.
To install dev + staging together:
- Android: add
applicationIdSuffix ".dev"(etc.) per flavor - iOS: use different
PRODUCT_BUNDLE_IDENTIFIERper scheme
Troubleshooting
| Problem | Fix |
|---|---|
| Wrong API URL at runtime | Metro and native build use different envs. Restart Metro with matching yarn start:*. |
| Env change not applied | Rebuild the native app after editing .env. |
| Android: variant not found | Use camelCase: --mode=stagingDebug, not staging-debug. |
| Android: Metro won't connect | Add your debug variant to debuggableVariants in build.gradle. |
| iOS: wrong app name | Clean build (⇧⌘K). Check scheme pre-action points to correct .env. |
| iOS: scheme missing in CI | Scheme must be in xcshareddata/xcschemes/ and marked Shared. |
| Android: empty Config values | envConfigFiles must be declared before dotenv.gradle. |
Project file map
| File | Purpose |
|---|---|
.env, .env.staging, .env.production | Secrets & config (gitignored) |
package.json | start:*, android:*, ios:* scripts |
android/app/build.gradle | Flavors, env mapping, debuggable variants |
android/app/src/main/AndroidManifest.xml | android:label="@string/app_name" |
ios/Config/Debug.xcconfig | Debug base config |
ios/Config/Release.xcconfig | Release base config |
ios/scripts/set-env-file.sh | Picks env file per scheme |
ios/<App>.xcodeproj/xcshareddata/xcschemes/ | Shared schemes (in git) |
src/config/env.ts | Typed JS access to env vars |
src/types/react-native-config.d.ts | TypeScript types for env keys |
Closing thoughts
Multi-environment setups pay off the first time QA needs staging while you keep developing locally. The upfront Gradle and Xcode configuration is tedious, but the daily workflow becomes as simple as picking the right yarn script pair.
If you're shipping React Native to production, this pattern scales cleanly — from two developers to CI pipelines that archive the correct scheme per branch.