File: variants.md | Updated: 11/15/2025
Hide navigation
Search
Ctrl K
Home Guides EAS Reference Learn
Archive Expo Snack Discord and Forums Newsletter
Copy page
Learn how to install multiple variants of an app on the same device.
Copy page
When creating development, preview, and production builds , installing these build variants simultaneously on the same device is common. This allows working in development, previewing the next version of the app, and running the production version on a device without needing to uninstall and reinstall the app.
This guide provides the steps required to configure multiple (development and production) variants to install and use them on the same device.
To have multiple variants of an app installed on your device, each variant must have a unique Application ID (Android) or Bundle Identifier (iOS) .
Configure development and production variants
You have created a project using Expo tooling, and now you want to create a development and a production build. Your project's app.json may have the following configuration:
app.json
Copy
{ "expo": { "name": "MyApp", "slug": "my-app", "ios": { "bundleIdentifier": "com.myapp" }, "android": { "package": "com.myapp" } } }
If your project has EAS Build configured, the eas.json also has a similar configuration as shown below:
eas.json
Copy
{ "build": { "development": { "developmentClient": true }, "production": {} } }
To have multiple variants of the app installed on the same device, rename the app.json to app.config.js and export the configuration as shown below:
app.config.js
Copy
export default { name: 'MyApp', slug: 'my-app', ios: { bundleIdentifier: 'com.myapp', }, android: { package: 'com.myapp', }, };
In app.config.js, add an environment variable called IS_DEV to switch the android.package and ios.bundleIdentifier for each variant based on the variable:
app.config.js
Copy
const IS_DEV = process.env.APP_VARIANT === 'development'; export default { name: IS_DEV ? 'MyApp (Dev)' : 'MyApp', slug: 'my-app', ios: { bundleIdentifier: IS_DEV ? 'com.myapp.dev' : 'com.myapp', }, android: { package: IS_DEV ? 'com.myapp.dev' : 'com.myapp', } };
In the above example, the environment variable IS_DEV is used to differentiate between the development and production environment. Based on its value, the different Application IDs or Bundle Identifiers are set for each variant.
Additional app variant customizations
You can customize other aspects of your app on a per-variant basis. You can swap any configuration that you used previously in app.json using the same approach as above.
Examples:
android.package and ios.bundleIdentifier.expo-dev-client plugin to disable the app scheme used by Expo CLI and EAS Update QR codes in non-development builds. This ensures that those URLs will always launch the development build, regardless of your device's defaults:app.config.js
Copy
plugins: [ [ 'expo-dev-client', { addGeneratedScheme: !!IS_DEV, }, ], ],
In eas.json, set the APP_VARIANT environment variable to run builds with the development profile by using the env property:
eas.json
Copy
{ "build": { "development": { "developmentClient": true, "env": { "APP_VARIANT": "development" } }, "production": {} } }
Now, when you run eas build --profile development, the environment variable APP_VARIANT is set to development when evaluating app.config.js both locally and on the EAS Build builder.
When you start your development server, you'll need to run APP_VARIANT=development npx expo start (or the platform equivalent if you use Windows).
A shortcut for this is to add the following script to your package.json:
package.json
Copy
{ "scripts": { "dev": "APP_VARIANT=development npx expo start" } }
When you run eas build --profile production the APP_VARIANT variable environment is not set, and the build runs as the production variant.
Note: If you use EAS Update to publish JavaScript updates of your app, you should be cautious to set the correct environment variables for the app variant that you are publishing for when you run the
eas updatecommand. See the EAS Build Environment variables and secrets for more information.
If you want to use your app config file as the source of truth for your app configuration (including bundle identifiers and package names for variants), you need to migrate to Continuous Native Generation (CNG) and add android and ios directories to your .gitignore file. This is especially important if you started with a React Native CLI project and added custom native code. This ensures that the native project configuration doesn't take precedence over your app config, especially when using bundle ID lookup behavior. Without this, you may experience issues where the wrong app variant runs or development builds aren't detected properly. Alternatively, you can explicitly opt for the Android flavors and iOS schemes as described below.
In android/app/build.gradle, create a separate flavor for every build profile from eas.json that you want to build.
android/app/build.gradle
Copy
android { %%placeholder-start%%... %%placeholder-end%% flavorDimensions "env" productFlavors { production { dimension "env" applicationId 'com.myapp' } development { dimension "env" applicationId 'com.myapp.dev' } } %%placeholder-start%%... %%placeholder-end%% }
Note: Currently, EAS CLI supports only the
applicationIdfield. If you useapplicationIdSuffixinsideproductFlavorsorbuildTypessections then this value will not be detected correctly.
Assign Android flavors to EAS Build profiles by specifying a gradleCommand in the eas.json:
eas.json
Copy
{ "build": { "development": { "android": { "gradleCommand": ":app:assembleDevelopmentDebug" } }, "production": { "android": { "gradleCommand": ":app:bundleProductionRelease" } } } }
By default, every flavor can be built in either debug or release mode. If you want to restrict some flavor to a specific mode, see the snippet below, and modify build.gradle.
android/app/build.gradle
Copy
android { %%placeholder-start%%... %%placeholder-end%% variantFilter { variant -> def validVariants = [ ["production", "release"], ["development", "debug"], ] def buildTypeName = variant.buildType*.name def flavorName = variant.flavors*.name def isValid = validVariants.any { flavorName.contains(it[0]) && buildTypeName.contains(it[1]) } if (!isValid) { setIgnore(true) } } %%placeholder-start%%... %%placeholder-end%% }
The rest of the configuration at this point is not specific to EAS, it's the same as it would be for any Android project with flavors. There are a few common configurations that you might want to apply to your project:
To change the name of the app built with the development profile, create a android/app/src/development/res/value/strings.xml file:
android/app/src/development/res/value/strings.xml
Copy
<resources> <string name="app_name">MyApp - Dev</string> </resources>
To change the icon of the app built with the development profile, create android/app/src/development/res/mipmap-* directories with appropriate assets (you can copy them from android/app/src/main/res and replace the icon files).
To specify google-services.json for a specific flavor, put it in the android/app/src/{flavor}/google-services.json file.
To configure sentry, add project.ext.sentryCli = [ flavorAware: true ] to android/app/build.gradle and name your properties file android/sentry-{flavor}-{buildType}.properties (for example, android/sentry-production-release.properties)
Assign a different scheme to every build profile in eas.json:
eas.json
Copy
{ "build": { "development": { "ios": { "buildConfiguration": "Debug", "scheme": "myapp-dev" } }, "production": { "ios": { "buildConfiguration": "Release", "scheme": "myapp" } } } }
Podfile should have a target defined like this:
Podfile
Copy
target 'myapp' do %%placeholder-start%%... %%placeholder-end%% end
Replace it with an abstract target, where the common configuration can be copied from the old target:
Podfile
Copy
abstract_target 'common' do # put common target configuration here target 'myapp' do end target 'myapp-dev' do end end
Open project in Xcode, click on the project name in the navigation panel, right click on the existing target, and click "Duplicate":

Rename the target to something more meaningful, for example, myapp copy -> myapp-dev.
Configure a scheme for the new target:
Product -> Scheme -> Manage schemes.myapp copy on the list.myapp copy -> myapp-dev..xcscheme files. To fix that, uncheck the "Shared" checkbox and check it again, after that new .xcscheme file should show up in the ios/myapp.xcodeproj/xcshareddata/xcschemes directory.
By default, the newly created target has separate Info.plist file (in the above example, it's ios/myapp copy-Info.plist). To simplify your project we recommend using the same file for all targets:
Build Settings tab.Packaging section.Product Bundle Identifier.
To change the display name:
Bundle display name with value $(DISPLAY_NAME).Build Settings for both targets and find User-Defined section.DISPLAY_NAME with the name you want to use for that target.To change the app icon:
AppIcon)Build Settings for the target that you want to change icon.Asset Catalog Compiler - Options section.Primary App Icon Set Name to the name of the new image set.