DEV Community

Prabhakar
Prabhakar

Posted on

React Native Environment Setup: Managing Dev, Prod, and Staging Builds with Android Flavors and iOS Schemes

If you've worked on a React Native app that has both development and production environments, you probably know how quickly environment management becomes annoying.

At the beginning, it usually looks simple:

Development → API A
Production  → API B
Enter fullscreen mode Exit fullscreen mode

Then the app grows.

Suddenly, you have different:

  • API endpoints
  • Firebase projects
  • Branch environments
  • Payment environments
  • Analytics configuration
  • Push notification configuration
  • Android package names
  • iOS bundle IDs
  • Native configuration

And switching between environments starts looking something like this:

Change API URL
Change Firebase file
Change package name
Change Branch config
Change payment config
Build
Remember to change everything back
Enter fullscreen mode Exit fullscreen mode

I wanted to get rid of that workflow.

My goal was simple:

npm run android:dev
npm run android:prod

npm run ios:dev
npm run ios:prod
Enter fullscreen mode Exit fullscreen mode

The command should decide the environment.

I shouldn't have to edit source files before every build.

This is how I approached it.


What I Wanted to Achieve

Before writing any configuration, I defined the workflow I wanted.

For Android:

npm run android:dev
npm run android:prod
Enter fullscreen mode Exit fullscreen mode

For iOS:

npm run ios:dev
npm run ios:prod
Enter fullscreen mode Exit fullscreen mode

And for release builds:

npm run build:apk:dev
npm run build:apk:prod
npm run build:aab:prod
Enter fullscreen mode Exit fullscreen mode

The important part is that these commands should automatically select the correct:

API
Firebase
Branch
Payment environment
App ID
App name
Native configuration
Enter fullscreen mode Exit fullscreen mode

without changing application code.


The Approach

I ended up using three pieces:

1. react-native-config

For environment variables.

2. Android Product Flavors

For creating separate dev and prod Android applications.

3. iOS Schemes + Build Configurations

For doing the equivalent thing on iOS.

The overall idea looks like this:

                 .env.dev
                    │
                    ▼
              Development
                    │
          ┌─────────┴─────────┐
          │                   │
       Android              iOS
       dev flavor         Dev scheme
          │                   │
          ▼                   ▼
       Dev App             Dev App


                 .env.prod
                    │
                    ▼
              Production
                    │
          ┌─────────┴─────────┐
          │                   │
       Android              iOS
      prod flavor         Prod scheme
          │                   │
          ▼                   ▼
       Prod App            Prod App
Enter fullscreen mode Exit fullscreen mode

Let's go through it step by step.


1. Start With .env Files

The first thing I did was separate environment-specific values.

.env.dev
.env.prod
Enter fullscreen mode Exit fullscreen mode

For example:

# .env.dev

APP_ENV=dev

API_ENDPOINT=https://api.dev.example.com

JUSPAY_ENVIRONMENT=sandbox

BRANCH_TEST_MODE=true

MOENGAGE_APP_ID=dev-app-id
Enter fullscreen mode Exit fullscreen mode

And:

# .env.prod

APP_ENV=prod

API_ENDPOINT=https://api.example.com

JUSPAY_ENVIRONMENT=production

BRANCH_TEST_MODE=false

MOENGAGE_APP_ID=prod-app-id
Enter fullscreen mode Exit fullscreen mode

The actual values will obviously be different for your application.

The important thing is that the application code doesn't need to know which file to use.

The build process will take care of that.


2. Why react-native-config?

I used react-native-config because some of the configuration was needed not only in JavaScript, but also in native Android and iOS code.

In JavaScript:

import Config from 'react-native-config';

console.log(Config.API_ENDPOINT);
Enter fullscreen mode Exit fullscreen mode

On Android:

BuildConfig.API_ENDPOINT
Enter fullscreen mode Exit fullscreen mode

On iOS:

[RNCConfig envFor:@"API_ENDPOINT"];
Enter fullscreen mode Exit fullscreen mode

This gives me one source of environment configuration that can be consumed from different parts of the application.

One thing to know upfront:

.env values are baked in at BUILD time
Enter fullscreen mode Exit fullscreen mode

There is no file reading at runtime. If you change a .env value, you rebuild the app.


3. Android Product Flavors

Android was the easier part because Gradle has a concept called Product Flavors.

I created two flavors:

dev
prod
Enter fullscreen mode Exit fullscreen mode

For example:

android {

    flavorDimensions "environment"

    productFlavors {

        dev {
            dimension "environment"
            applicationId "com.example.app.dev"
        }

        prod {
            dimension "environment"
            applicationId "com.example.app"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now Android knows that these are two different applications.

That also means I can install both on the same device:

MyApp Dev
MyApp
Enter fullscreen mode Exit fullscreen mode

This is extremely useful during development.

I don't have to uninstall the production version just to test the development version.


4. Understanding Android Variants

This was one of the things that initially confused me.

Android doesn't just have:

dev
prod
Enter fullscreen mode Exit fullscreen mode

because each flavor can be combined with a build type.

So we get:

devDebug
devRelease

prodDebug
prodRelease
Enter fullscreen mode Exit fullscreen mode

Think of it like:

Flavor + Build Type = Variant
Enter fullscreen mode Exit fullscreen mode

For example:

dev + debug = devDebug

prod + release = prodRelease
Enter fullscreen mode Exit fullscreen mode

Once you understand this, commands such as:

./gradlew assembleProdRelease
Enter fullscreen mode Exit fullscreen mode

become much easier to understand.


5. Connect Android Flavors to .env Files

Now we need to tell react-native-config which environment file belongs to which flavor.

In android/app/build.gradle:

project.ext.envConfigFiles = [
    devdebug: ".env.dev",
    devrelease: ".env.dev",

    proddebug: ".env.prod",
    prodrelease: ".env.prod",
]

apply from: project(':react-native-config')
    .projectDir.getPath() + "/dotenv.gradle"
Enter fullscreen mode Exit fullscreen mode

Now the mapping is:

devDebug    → .env.dev
devRelease  → .env.dev

prodDebug   → .env.prod
prodRelease → .env.prod
Enter fullscreen mode Exit fullscreen mode

This is the part that removes a lot of manual work.

When I build devDebug, the development environment is automatically selected.

When I build prodRelease, the production environment is automatically selected.


6. Different App Names and Package IDs

I also wanted the two applications to be visually distinguishable.

For example:

MyApp Dev
MyApp
Enter fullscreen mode Exit fullscreen mode

and:

com.example.app.dev
com.example.app
Enter fullscreen mode Exit fullscreen mode

You can configure the app name per flavor:

productFlavors {

    dev {
        dimension "environment"
        applicationId "com.example.app.dev"

        resValue "string", "app_name", "MyApp Dev"
    }

    prod {
        dimension "environment"
        applicationId "com.example.app"

        resValue "string", "app_name", "MyApp"
    }
}
Enter fullscreen mode Exit fullscreen mode

One small thing to remember here:

If app_name already exists in strings.xml, remove it from there.

strings.xml app_name  +  flavor resValue app_name
            ↓
   duplicate resource build error
Enter fullscreen mode Exit fullscreen mode

Now I can immediately tell which application I'm opening.

This sounds like a small thing, but when both apps are installed on a device, it makes a big difference.


7. Separate Firebase for Dev and Prod

This was another important part.

I didn't want the development application sending Crashlytics, Analytics, or other Firebase data to the production Firebase project.

Instead of manually replacing google-services.json, I used Android's flavor-specific directories.

The structure looks like:

android/app/src/

├── main/
│
├── dev/
│   └── google-services.json
│
└── prod/
    └── google-services.json
Enter fullscreen mode Exit fullscreen mode

Now:

devDebug
   ↓
src/dev/google-services.json
Enter fullscreen mode Exit fullscreen mode

and:

prodRelease
   ↓
src/prod/google-services.json
Enter fullscreen mode Exit fullscreen mode

Gradle handles the selection based on the flavor.

This was a big improvement because Firebase configuration stopped being something I had to remember to change manually.


8. Flavor-Specific Android Manifest

The same approach works for other Android-specific configuration.

For example, suppose Branch has a test mode for development and live mode for production.

I can have:

android/app/src/dev/AndroidManifest.xml
android/app/src/prod/AndroidManifest.xml
Enter fullscreen mode Exit fullscreen mode

The development manifest can contain:

<meta-data
    android:name="io.branch.sdk.TestMode"
    android:value="true" />
Enter fullscreen mode Exit fullscreen mode

while production can contain:

<meta-data
    android:name="io.branch.sdk.TestMode"
    android:value="false" />
Enter fullscreen mode Exit fullscreen mode

Now:

Dev build  → test environment
Prod build → live environment
Enter fullscreen mode Exit fullscreen mode

Again, no manual switching.


9. One react-native-config Issue I Ran Into

This was one of those issues that took some time to figure out.

If your Android namespace and applicationId don't match, react-native-config can have trouble finding the generated BuildConfig class.

In that situation, you may need:

resValue "string", "build_config_package", "com.example.app"
Enter fullscreen mode Exit fullscreen mode

The important distinction is:

applicationId
    ↓
Identifies the Android application

namespace
    ↓
Defines where Android generates classes
Enter fullscreen mode Exit fullscreen mode

If these don't line up with what react-native-config expects, you can end up with something like:

console.log(Config);
Enter fullscreen mode Exit fullscreen mode

returning missing or empty values.

And then you may start seeing errors that look completely unrelated, such as API/network failures.

So if react-native-config suddenly appears not to work on Android, this is worth checking.

One More Issue I Ran Into With release builds:

ProGuard can strip BuildConfig fields, because react-native-config reads them through reflection.

Add this to android/app/proguard-rules.pro:

-keep class com.example.app.BuildConfig { *; }
Enter fullscreen mode Exit fullscreen mode

Use your namespace here, not the dev applicationId.

Without it, debug builds work and release builds mysteriously get empty values.


10. Tell React Native Which Variant Is Debuggable

Another small configuration is required because React Native needs to know which flavor should connect to Metro.

For example:

react {
    debuggableVariants = ["devDebug"]

    autolinkLibrariesWithApp()
}
Enter fullscreen mode Exit fullscreen mode

Now React Native knows:

devDebug → development build → use Metro
Enter fullscreen mode Exit fullscreen mode

while release builds can bundle the JavaScript normally.

This becomes especially important once you introduce custom flavors.

If you skip this, devDebug bundles the JavaScript instead of connecting to Metro — and you lose fast refresh without any error message.


11. iOS Works Differently

Android gives us Product Flavors.

iOS doesn't have exactly the same concept.

For iOS, I used:

  • Build Configurations
  • Schemes
  • Build Settings

The idea is still the same:

Dev
Prod
Enter fullscreen mode Exit fullscreen mode

but the implementation is different.

For example:

Debug
Release

Dev.Debug
Dev.Release
Enter fullscreen mode Exit fullscreen mode

Then I created two schemes:

MyApp
MyApp-Dev
Enter fullscreen mode Exit fullscreen mode

The production scheme uses:

Debug
Release
Enter fullscreen mode Exit fullscreen mode

while the development scheme uses:

Dev.Debug
Dev.Release
Enter fullscreen mode Exit fullscreen mode

So:

MyApp-Dev
     ↓
Dev.Debug
     ↓
Development environment
Enter fullscreen mode Exit fullscreen mode

and:

MyApp
     ↓
Release
     ↓
Production environment
Enter fullscreen mode Exit fullscreen mode

To create the configurations in Xcode:

Project → Info tab → Configurations
    ↓
Duplicate "Debug"   → "Dev.Debug"
Duplicate "Release" → "Dev.Release"
Enter fullscreen mode Exit fullscreen mode

And mark both schemes as Shared, so your teammates and CI can see them too.


12. Different iOS Bundle IDs

Just like Android has different applicationIds, iOS can have different bundle IDs.

For example:

Production:
com.example.app

Development:
com.example.app.dev
Enter fullscreen mode Exit fullscreen mode

Set PRODUCT_BUNDLE_IDENTIFIER per configuration in Build Settings:

Debug / Release          → com.example.app
Dev.Debug / Dev.Release  → com.example.app.dev
Enter fullscreen mode Exit fullscreen mode

This means both applications can exist on the same iPhone:

MyApp
MyApp Dev
Enter fullscreen mode Exit fullscreen mode

That makes testing production and development side by side much easier.

For the app name, add a user-defined build setting APP_DISPLAY_NAME per configuration, and reference it in Info.plist:

<key>CFBundleDisplayName</key>
<string>$(APP_DISPLAY_NAME)</string>
Enter fullscreen mode Exit fullscreen mode

13. Selecting the .env File on iOS

This part required a slightly different approach.

With Android, we mapped flavors directly to .env files.

For iOS, I used a Scheme Pre-action.

For the development scheme:

cp "${PROJECT_DIR}/../.env.dev" \
   "${PROJECT_DIR}/../.env"
Enter fullscreen mode Exit fullscreen mode

For production:

cp "${PROJECT_DIR}/../.env.prod" \
   "${PROJECT_DIR}/../.env"
Enter fullscreen mode Exit fullscreen mode

So the flow becomes:

MyApp-Dev
    ↓
.env.dev
    ↓
.env
Enter fullscreen mode Exit fullscreen mode

and:

MyApp
    ↓
.env.prod
    ↓
.env
Enter fullscreen mode Exit fullscreen mode

Then react-native-config reads .env.

This is one of the main differences between my Android and iOS setup.

One easy-to-miss detail:

In the pre-action, set "Provide build settings from" to your app target.

Not set → ${PROJECT_DIR} is empty → copy silently fails → stale .env
Enter fullscreen mode Exit fullscreen mode

14. Separate Firebase Configuration on iOS

The same environment separation is needed for Firebase on iOS.

Instead of having one GoogleService-Info.plist, I kept separate files for each environment.

For example:

Firebase/

├── Dev/
│   └── GoogleService-Info.plist
│
└── Prod/
    └── GoogleService-Info.plist
Enter fullscreen mode Exit fullscreen mode

Then the Xcode build process copies the appropriate file into the application bundle.

I did that with a small Build Phase run script (placed before any Firebase build phases):

if [[ "${CONFIGURATION}" == *"Dev"* ]]; then
  cp "${SRCROOT}/MyApp/Firebase/Dev/GoogleService-Info.plist" \
     "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
else
  cp "${SRCROOT}/MyApp/Firebase/Prod/GoogleService-Info.plist" \
     "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
fi
Enter fullscreen mode Exit fullscreen mode

Conceptually:

Dev scheme
    ↓
Dev GoogleService-Info.plist
    ↓
Dev Firebase project
Enter fullscreen mode Exit fullscreen mode

and:

Prod scheme
    ↓
Prod GoogleService-Info.plist
    ↓
Prod Firebase project
Enter fullscreen mode Exit fullscreen mode

This keeps development and production data separated.

One warning from experience:

Both plist files must be real files from the Firebase console.

A placeholder file with dummy values crashes the app on launch, inside [FIRApp configure].

Also update your Podfile so CocoaPods knows about the new configurations:

project 'MyApp.xcodeproj', {
  'Debug' => :debug,
  'Release' => :release,
  'Dev.Debug' => :debug,
  'Dev.Release' => :release,
}
Enter fullscreen mode Exit fullscreen mode

Then run pod install.


15. Using the Configuration From JavaScript

Once all of this is configured, application code becomes much cleaner.

Instead of:

const API_URL =
    isDevelopment
        ? "https://api.dev.example.com"
        : "https://api.example.com";
Enter fullscreen mode Exit fullscreen mode

I can simply do:

import Config from 'react-native-config';

export const CONFIG = {
    API_ENDPOINT: Config.API_ENDPOINT,
    JUSPAY_ENVIRONMENT: Config.JUSPAY_ENVIRONMENT,
    BRANCH_TEST_MODE: Config.BRANCH_TEST_MODE,
    MOENGAGE_APP_ID: Config.MOENGAGE_APP_ID,
};
Enter fullscreen mode Exit fullscreen mode

The application doesn't need to decide whether it is dev or prod.

The build already made that decision.

That's the part I like most about this setup.

One small thing to remember:

Every .env value arrives in JavaScript as a string.

BRANCH_TEST_MODE=false
        ↓
Config.BRANCH_TEST_MODE === "false"   (a truthy string!)
Enter fullscreen mode Exit fullscreen mode

So convert booleans explicitly:

const isTestMode = Config.BRANCH_TEST_MODE === 'true';
Enter fullscreen mode Exit fullscreen mode

16. Native Code Can Use the Same Values

The same configuration can be consumed by native code.

For Android:

val appId = BuildConfig.MOENGAGE_APP_ID
Enter fullscreen mode Exit fullscreen mode

For iOS:

NSString *appId =
    [RNCConfig envFor:@"MOENGAGE_APP_ID"];
Enter fullscreen mode Exit fullscreen mode

Careful with the class name on iOS:

RNCConfig          → correct
ReactNativeConfig  → wrong (it's the module name, not the class)
Enter fullscreen mode Exit fullscreen mode

The wrong name still compiles (Objective-C is forgiving), but fails at runtime. This one cost me an afternoon.

This is useful when initializing native SDKs.

For example:

Firebase
Branch
MoEngage
Payment SDKs
Push notifications
Enter fullscreen mode Exit fullscreen mode

can all have environment-specific configuration.


17. Hide the Complexity Behind npm Scripts

At this point, the underlying setup had become fairly complicated.

But I didn't want every developer to remember Gradle variants and Xcode scheme names.

So I created simple npm scripts.

For example:

{
  "scripts": {
    "android:dev":
      "react-native run-android --mode=devDebug --appId=com.example.app.dev",

    "android:prod":
      "react-native run-android --mode=prodDebug --appId=com.example.app",

    "ios:dev":
      "react-native run-ios --scheme MyApp-Dev",

    "ios:prod":
      "react-native run-ios --scheme MyApp",

    "build:apk:dev":
      "cd android && ./gradlew assembleDevRelease",

    "build:apk:prod":
      "cd android && ./gradlew assembleProdRelease",

    "build:aab:prod":
      "cd android && ./gradlew bundleProdRelease"
  }
}
Enter fullscreen mode Exit fullscreen mode

Two flags worth explaining:

--mode
    ↓
The Gradle variant to build (devDebug, prodDebug, ...)
Newer React Native CLI uses --mode, not the old --variant

--appId
    ↓
Which app to LAUNCH after installing
Without it, the dev app installs but never opens
Enter fullscreen mode Exit fullscreen mode

Now developers only need to remember:

npm run android:dev
Enter fullscreen mode Exit fullscreen mode

instead of:

./gradlew assembleDevDebug
Enter fullscreen mode Exit fullscreen mode

And:

npm run build:aab:prod
Enter fullscreen mode Exit fullscreen mode

instead of remembering the exact Gradle task.

It's a small abstraction, but it makes the workflow much nicer.


18. What Actually Happens When I Run a Dev Build?

This is the part that helped me understand the whole setup.

When I run:

npm run android:dev
Enter fullscreen mode Exit fullscreen mode

the flow is roughly:

npm run android:dev
        ↓
     devDebug
        ↓
    .env.dev
        ↓
react-native-config
        ↓
 ┌──────┴───────┐
 │              │
JavaScript    Android
 │              │
 └──────┬───────┘
        ↓
     Dev App
        ↓
 ┌──────┼───────────┐
 │      │           │
API   Firebase    Branch
Enter fullscreen mode Exit fullscreen mode

I don't have to change anything manually.

That's the real benefit.


19. What About Metro?

One question that came up while setting this up was:

Do I need separate Metro servers for dev and prod?

No.

Metro is mainly responsible for bundling JavaScript.

The environment is selected by the application build.

So conceptually:

                 Metro
                   │
          ┌────────┴────────┐
          │                 │
       Dev App           Prod App
Enter fullscreen mode Exit fullscreen mode

The important thing is that the native build determines which environment configuration gets injected.

You can start Metro once and build dev, then prod, then dev again against the same instance.


20. When I Clear the Metro Cache

Sometimes after switching branches or changing environment configuration, things can look stale.

In those situations, I use:

npx react-native start --reset-cache
Enter fullscreen mode Exit fullscreen mode

or keep a script such as:

{
  "scripts": {
    "start:clean":
      "react-native start --reset-cache"
  }
}
Enter fullscreen mode Exit fullscreen mode

It's particularly useful when:

  • environment values appear stale
  • you switched branches
  • Metro is behaving strangely
  • module resolution starts acting unexpectedly

But remember: changing an environment variable doesn't magically change an already-built native application.

Usually, you need to rebuild the app.


21. Building Release APKs

For a development release APK:

npm run build:apk:dev
Enter fullscreen mode Exit fullscreen mode

For a production release APK:

npm run build:apk:prod
Enter fullscreen mode Exit fullscreen mode

And for a Play Store bundle:

npm run build:aab:prod
Enter fullscreen mode Exit fullscreen mode

Under the hood, these map to Gradle tasks such as:

./gradlew assembleDevRelease
./gradlew assembleProdRelease
./gradlew bundleProdRelease
Enter fullscreen mode Exit fullscreen mode

The important thing is that the correct flavor is part of the build command.

So:

assembleDevRelease
        ↓
.env.dev
        ↓
Dev application
Enter fullscreen mode Exit fullscreen mode

while:

bundleProdRelease
        ↓
.env.prod
        ↓
Production application
Enter fullscreen mode Exit fullscreen mode

22. How I Verify the Environment

One thing I learned while debugging this setup:

Don't assume the environment is correct. Verify it.

During development, I sometimes temporarily log:

console.warn('ENV:', Config.APP_ENV);
console.warn('API:', Config.API_ENDPOINT);
console.warn(
    'Payment:',
    Config.JUSPAY_ENVIRONMENT
);
Enter fullscreen mode Exit fullscreen mode

For a dev build, I should see something like:

ENV: dev
API: https://api.dev.example.com
Payment: sandbox
Enter fullscreen mode Exit fullscreen mode

For production:

ENV: prod
API: https://api.example.com
Payment: production
Enter fullscreen mode Exit fullscreen mode

This is much faster than trying to figure out from an API error which environment the app is actually using.

And on Android, you can confirm both apps are installed side by side:

adb shell pm list packages | grep example
# expected: com.example.app.dev AND com.example.app
Enter fullscreen mode Exit fullscreen mode

23. Adding a New Environment Variable

Once the setup is working, adding another variable is straightforward.

Suppose we need:

NEW_API_KEY
Enter fullscreen mode Exit fullscreen mode

Add it to both files:

# .env.dev
NEW_API_KEY=dev-value
Enter fullscreen mode Exit fullscreen mode
# .env.prod
NEW_API_KEY=prod-value
Enter fullscreen mode Exit fullscreen mode

Then:

import Config from 'react-native-config';

const apiKey = Config.NEW_API_KEY;
Enter fullscreen mode Exit fullscreen mode

If native code needs it, expose/use it there as well.

The important thing is to keep the variable name consistent:

.env.dev
     ↓
.env.prod
     ↓
JavaScript
     ↓
Native code
Enter fullscreen mode Exit fullscreen mode

And rebuild — remember, values are baked in at build time.


24. What My Final Workflow Looks Like

After putting all of this together, my daily workflow becomes very simple.

Working on development

npm run android:dev
Enter fullscreen mode Exit fullscreen mode

or:

npm run ios:dev
Enter fullscreen mode Exit fullscreen mode

Testing the production environment locally

npm run android:prod
Enter fullscreen mode Exit fullscreen mode

or:

npm run ios:prod
Enter fullscreen mode Exit fullscreen mode

Creating a development APK

npm run build:apk:dev
Enter fullscreen mode Exit fullscreen mode

Creating a production APK

npm run build:apk:prod
Enter fullscreen mode Exit fullscreen mode

Creating a production AAB

npm run build:aab:prod
Enter fullscreen mode Exit fullscreen mode

That's the workflow I was trying to achieve from the beginning.


25. What I Like About This Approach

The biggest improvement isn't actually the commands.

It's the fact that the environment selection became predictable.

Before:

Developer
   ↓
Remember what needs to change
   ↓
Change several files
   ↓
Build
   ↓
Hope nothing was missed
Enter fullscreen mode Exit fullscreen mode

After:

Developer
   ↓
Choose dev or prod
   ↓
Build system selects configuration
   ↓
Correct app is built
Enter fullscreen mode Exit fullscreen mode

That removes a whole category of mistakes.


26. A Few Things I Learned

Don't treat .env as the entire environment configuration

Some configuration lives outside JavaScript.

Firebase files, Android manifests, bundle IDs, package IDs, and native SDK configuration may also need to be environment-specific.

So think about the whole build, not just API URLs.

Give dev and prod different app IDs

This lets you install both applications on the same device.

It's one of the most useful parts of having flavors/schemes.

Keep Firebase projects separate

Your development app shouldn't accidentally send analytics, Crashlytics, or other data to production.

Make the commands boring

A good build system should make the common workflow boring:

npm run android:dev
Enter fullscreen mode Exit fullscreen mode

is much easier to remember than a collection of Gradle and Xcode commands.

Verify before debugging

If an API call suddenly starts failing, first check:

Which environment am I running?
Which API endpoint am I using?
Which Firebase project am I using?
Enter fullscreen mode Exit fullscreen mode

It can save a surprising amount of time.

Fail toward the safe value

For anything payment-related, add a fallback:

environment: Config.JUSPAY_ENVIRONMENT || 'production'
Enter fullscreen mode Exit fullscreen mode

If a variable is ever missing, defaulting to production is safer than crashing — or worse, silently using sandbox in a real build.


Final Setup

The final architecture is basically:

                         React Native
                              │
                 ┌────────────┴────────────┐
                 │                         │
                Dev                       Prod
                 │                         │
             .env.dev                  .env.prod
                 │                         │
        ┌────────┴────────┐       ┌────────┴────────┐
        │                 │       │                 │
     Android             iOS    Android             iOS
     dev flavor       Dev scheme prod flavor       Prod scheme
        │                 │       │                 │
        ▼                 ▼       ▼                 ▼
     Dev App           Dev App  Prod App         Prod App
Enter fullscreen mode Exit fullscreen mode

And the developer experience becomes:

npm run android:dev
npm run android:prod

npm run ios:dev
npm run ios:prod
Enter fullscreen mode Exit fullscreen mode

The key idea is simple:

Don't make developers manually switch environments. Make the build system do it.

Once you have this foundation, adding another environment like qa or staging becomes a matter of adding the corresponding flavor/scheme and configuration rather than rewriting application logic.


References

Top comments (0)