In a bare React Native app, how do you let staging and production builds install side by side, each pointing at its own API?
answer
- one app identity per environment
- Android flavor: applicationIdSuffix
- iOS: configuration plus scheme
- bundle ID per configuration
- re-run pod install for new configs
basics
~20 sGive each environment its own native variant and app identity: Android product flavors with applicationIdSuffix, iOS build configurations with their own bundle identifier selected by a scheme. Each variant then feeds its own config file, which carries the API URL into the build.
solid answer
~40 sSide by side means different IDs, because two builds with the same application ID or bundle identifier are the same app. On Android I add product flavors in `android/app/build.gradle`: `staging` gets `applicationIdSuffix ".staging"` and its own `app_name` via `resValue`, producing variants like `stagingDebug` and `productionRelease`. On iOS I duplicate `Release` as a `Staging` configuration, give it its own `PRODUCT_BUNDLE_IDENTIFIER` and display name, add a staging scheme whose Run and Archive actions use it, and re-run `pod install` so CocoaPods knows the configuration. Each variant then selects its own `.env` for a config library such as `react-native-config`, so the API URL comes from the build, never from `__DEV__`. Signing, push credentials and link associations each need a staging copy too.
code
bash · 2 linesnpx react-native run-android --mode stagingDebug
npx react-native run-ios --mode Staginggo deeper
Recall that the OS identifies an app by its application ID or bundle identifier, so two environments need two IDs to coexist.
Walk through Android flavors with applicationIdSuffix and iOS configurations plus schemes, and how each variant selects its own config file.
Show the follow-through: pod install and the Podfile mapping, a configuration name the bundling script treats correctly, and staging copies of signing, push and link setup.
Decide how many environments earn their own app identity, weighing tester clarity against the credentials, provisioning and review work each new ID adds.
## The goal A food-delivery team wants three things from its staging and production builds: 1. both installed **on the same phone** at once, so a tester can compare them; 2. each pointing at **its own API**, with no chance of staging traffic hitting production; 3. each **visibly different** on the home screen, so nobody places a real order in the wrong app. The first requirement is the one people miss: Android and iOS identify an installed app by its **application ID** or **bundle identifier**. Two builds with the same ID are the same app — installing one replaces the other and they share storage, keychain entries and push registration. So each environment needs its own ID, and that is decided by the native projects, not by JavaScript. ## Android: product flavors In the `android/app/build.gradle` of a bare React Native project, **product flavors** describe variants of the app. Combined with the build types (`debug`, `release`, and since React Native 0.82 `debugOptimized`), they produce **build variants** such as `stagingDebug` and `productionRelease`. ```groovy android { flavorDimensions "env" productFlavors { staging { dimension "env" applicationIdSuffix ".staging" resValue "string", "app_name", "Food (Staging)" } production { dimension "env" } } } ``` - `applicationIdSuffix` appends to the base application ID, so the staging app installs beside production. - `resValue` gives each flavor its own launcher label; if the manifest's label reads `@string/app_name`, remove the fixed entry from `strings.xml` so the two definitions do not clash. - Per-flavor source sets can hold flavor-specific resources and config files. Build or run a variant with the React Native CLI's `--mode` option, for example `npx react-native run-android --mode stagingDebug`. ## iOS: build configurations and schemes Xcode separates two ideas: | Concept | What it is | What you do for staging | |---|---|---| | **Build configuration** | a named set of build settings (`Debug`, `Release`) | duplicate one as `Staging` | | **Build setting** per configuration | e.g. `PRODUCT_BUNDLE_IDENTIFIER`, display name | give `Staging` its own bundle ID and name | | **Scheme** | which configuration each action (Run, Archive) uses | add a staging scheme whose actions use `Staging` | Two React Native specifics follow: - **CocoaPods must learn the new configuration.** Run `pod install` again so the Pods project gets a matching configuration, and tell the Podfile whether `Staging` is debug-like or release-like: React Native's pod post-install step adds the `NDEBUG` flag only to configurations CocoaPods treats as release. - **The JS bundling script reads the configuration name.** React Native's "Bundle React Native code and images" phase treats any configuration whose name contains `Debug` as a development build and everything else as a release bundle, so the name you choose matters. Run it with `npx react-native run-ios --mode Staging`, or pick the scheme in Xcode. ## Getting the API URL to the JavaScript The native variant chooses **which config file** feeds the build. A config library such as `react-native-config` can read a per-variant `.env` file during the native build and expose values to JS; a Babel plugin such as `react-native-dotenv` inlines values when Metro transforms the code. Either way the app should read the URL from one config module, never decide it with `__DEV__`. ## Everything tied to the bundle ID A new bundle ID is a new app to every service that identifies apps by it. Expect a staging copy of each: - signing identity and provisioning for the staging ID; - push notification credentials and registration; - universal link and app link associations for a staging domain; - per-app configuration files that client SDKs load from the native project. ## In an Expo project Expo projects usually express the same split in a dynamic app config and EAS build profiles instead of hand-edited native projects; those mechanisms have their own owners. The underlying rule is identical: one app identity per environment.
- Why not keep one application ID and only swap the API URL between staging and production?Then the builds are the same app to the OS: installing staging replaces production, they share storage and keychain data, and push tokens and deep links cannot tell them apart. Testers can no longer compare the two, and a staging login can leak into a production install.
- What besides the API URL usually needs a staging copy once the bundle ID differs?Anything keyed by the app's identity: the signing identity and provisioning for the new ID, push notification credentials, universal link and app link associations for the staging domain, and any per-app config files that client SDKs read from the native project.
saying these in an interview costs you the question
- Staging and production can share one bundle ID if the API URL differs
- applicationIdSuffix only changes the launcher label
- An iOS scheme alone gives the app a different bundle identifier
- A new Xcode configuration needs no pod install
- The JavaScript bundle decides which app ID gets installed