App Flavors with Expo & EAS
Learn how to structure development, preview, and production variants of your Expo app using EAS build profiles and app config. This guide shows how to create installable app flavors, avoid common pitfalls, and decide when build-time variants are the right tool—and when they aren’t.
Julian Kwast
Senior Frontend Engineer
March 3, 2026
When building a mobile app you might think there’s only one codebase and one app that you develop and at some point you hit that sweet “Publish”-button. This might be true for many apps, but especially in React Native and even more so for Expo apps it is very common and desired to have one app for development, one for a preview environment, one for production and maybe even more. You might think of it as different instances or variants of the same app, but most of the time they are called Flavors.
Having these flavors allows for a build to be pushed to TestFlight or testing tracks of GooglePlay without interfering with the actual release management of the final version intended for production. These variants can then use e.g. a different backend instance to run against, have additional debug information available, or have a different set of features enabled through a feature flag system. All while neatly separated from your production app.
In this post we will go through the basic setup as well as some ideas on what other kinds of app flavors besides the preview flavor might be interesting.
How to make your app flavorful
At the base of it are two different configs. The app.config.ts (or app.json) and the eas.json.
In the eas.json you define build profiles, which determine how an app is build, and in app.config.ts you then add configuration for the variants that the different builds result in.
Let’s take a look at a basic version for the eas.json first, as that determines what profiles exist, and afterwards we will take a look at how to build them into full on flavors in app.config.ts.
Configuring flavors in eas.json
In the example below you see a build config in eas.json that includes the basic config for three flavors: development, preview and production.
Additionally there is a storebase entry that contains shared properties between the preview and production configurations. In this example would be just as easy to have those two properties directly added to those profiles, but in case your flavor shelf grows bigger and more complex, it is good to know that this option exists.
{
"build": {
"storebase": {
"distribution": "store",
"autoIncrement": true,
},
"development": { // <-- Name of the profile
"developmentClient": true,
"distribution": "internal",
"env": {
"APP_VARIANT": "development" // <--- This is key!
}
},
"preview": {
"extends": "storebase",
"android": {
"buildType": "apk"
},
"env": {
"APP_VARIANT": "preview"
}
},
"production": {
"extends": "storebase",
"android": {
"buildType": "app-bundle"
},
"env": {
"APP_VARIANT": "production"
}
}
}
}But what exactly does any of the content of this eas.json mean? To avoid derailing this post, we’ll only focus on the parts relevant to app flavors.
At this moment the things that are of most interest for us are the profile names development, preview and production. Having these present in eas.json means we can use the eas cli to create app builds using these three profiles using e.g.
npx --yes eas-cli@latest build --profile development💡
To use this you need to have an Expo account for EAS, and either let it generate a project for you upon first execution or link your codebase to an existing project.
New to Expo & EAS? Here is a link to the official docs, to get you started. If you find yourself stuck, feel free to reach out to us. We are happy to help!
Since developmentClient is set to true in the development profile, this will create an Expo development client for your app, which will have env.APP_VARIANT: "development" during prebuild execution.
This APP_VARIANT property is the key to implementing app flavors.
Using APP_VARIANT to implement flavors in app.config.ts
If you still do not have an app.config.ts but only an app.json we recommend transforming the app.json now. While you can have app.config.ts and app.json in parallel, it can lead to oversights/confusion as both are responsible for the final result of the configuration used for building the app.
If you just created a new Expo app, then you likely have a file that looks somewhat like this:
import { ExpoConfig } from 'expo/config';
export default (): ExpoConfig => ({
name: 'YourApp',
slug: 'your-app',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
userInterfaceStyle: 'light',
newArchEnabled: true,
splash: {
image: './assets/splash-icon.png',
resizeMode: 'contain',
backgroundColor: '#ffffff',
},
ios: {
supportsTablet: true,
},
android: {
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#ffffff',
},
edgeToEdgeEnabled: true,
predictiveBackGestureEnabled: false,
},
web: {
favicon: './assets/favicon.png',
},
});To add flavors to this, we simply need to make use of the APP_VARIANT that we set for ourselves as part of the eas.json.
The APP_VARIANT will be added to the process environment by the EAS cli, and can be accessed like any other environment variable using process.env.APP_VARIANT.
type AppVariant = 'development' | 'preview' | 'production';
function getAppEnv(): AppVariant {
const raw = process.env.APP_VARIANT;
if (raw === 'development' || raw === 'preview' || raw === 'production') return raw;
throw new Error('In your case you might have a reasonable fallback here');
}Using this we can safely access the APP_VARIANT and provide a fallback or throw an error in case something is amiss, and use this to manipulate the resulting configuration like so:
const variantConfig: Record<
AppVariant,
{
name: string;
iosBundleIdentifier: string;
androidPackage: string;
}
> = {
development: {
name: 'YourApp (Development)',
iosBundleIdentifier: 'com.yourcompany.myapp.development',
androidPackage: 'com.yourcompany.myapp.development',
},
preview: {
name: 'YourApp (Preview)',
iosBundleIdentifier: 'com.yourcompany.myapp.preview',
androidPackage: 'com.yourcompany.myapp.preview',
},
production: {
name: 'YourApp',
iosBundleIdentifier: 'com.yourcompany.myapp',
androidPackage: 'com.yourcompany.myapp',
},
};
const configForVariant = variantConfig[getAppEnv()];
export default (): ExpoConfig => ({
name: configForVariant.name,
slug: 'your-app',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
userInterfaceStyle: 'light',
newArchEnabled: true,
splash: {
image: './assets/splash-icon.png',
resizeMode: 'contain',
backgroundColor: '#ffffff',
},
ios: {
bundleIdentifier: configForVariant.iosBundleIdentifier,
supportsTablet: true,
},
android: {
package: configForVariant.androidPackage,
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#ffffff',
},
edgeToEdgeEnabled: true,
predictiveBackGestureEnabled: false,
},
web: {
favicon: './assets/favicon.png',
},
});Using this architecture you can clearly see what is and what is not considered specific to flavors, and easily extend this to have more properties defined by flavors, or add new flavors altogether.
💡
Neither the APP_VARIANT nor app.config.ts are bundled and available through metro during the development process. A change always requires a new prebuild (and therefore build) to be taking effect!
Extending your flavor palette
Sometimes just adding the previous three flavors might still be a bit bland, so here are some additional ideas to spice it up.
White labelling
If you are working on a project that uses separate app instances for different clients, you are usually faced with challenges on where you can use the same code and where you need to properly split the code base to allow for the flexibility that you need.
I don’t want to go too deep down the rabbit hole here, as that would lead us to content worth multiple blog posts on its own.
But some very frequent requirements for white labelling projects are:
- App Icon & Splash Screen
- Theming & Branding
- Translations / Text Adjustments
- Enabling/Disabling certain features
As you probably noticed, the easiest part here are the app icons and the splash screens, as they are directly (and usually only) referenced in the app.config.ts.
In case you don’t have a need for translations to be provided at runtime e.g. through a CMS, you might consider having them statically determined at build time using the app.config.ts as well.
To do that you can leverage the extra field in the config. To illustrate this, here is an example that takes care of providing the relevant information for theming in app.config.ts.
type AppVariant = 'companyAProduction' | 'companyBProduction';
function getAppEnv(): AppVariant {
const raw = process.env.APP_VARIANT;
if (raw === 'companyAProduction' || raw === 'companyBProduction') return raw;
throw new Error('In your case you might have a reasonable fallback here')
}
const variantConfig: Record<
AppVariant,
{
theme: 'companyA' | 'companyB';
}
> = {
companyAProduction: {
theme: 'companyA',
},
companyBProduction: {
theme: 'companyB',
},
};
const configForVariant = variantConfig[getAppEnv()];
export default (): ExpoConfig => ({
// ...
extra: {
theme: configForVariant.theme,
},
});
In this example we have the client (companyA/companyB) and environment (development/production) grouped in one APP_VARIANT variable, but depending on your situation it might be reasonable to separate them.
This can then later be accessed in the files by using expo-constants. (You might need to install expo-constants if you haven’t yet.)
import Constants from 'expo-constants';
import { Text } from 'react-native';
import { useTheme } from './myThemeImplementation';
const theme = Constants.expoConfig?.extra?.theme;
export const HelloWorld = () => {
const { colors } = useTheme(theme);
return <Text style={{ color: colors.text }}>Hello World!</Text>;
};Be aware that the type of Constants.expoConfig?.extra is not inferred from the app.config.ts!
If you want to increase your type safety you can define a type to help with this like so:
// appConfigExtra.ts
export type AppConfigExtra = {
theme: 'companyA' | 'companyB';
};
// app.config.ts
export default (): ExpoConfig => ({
// ...
extra: {
theme: configForVariant.theme,
} satisfies AppConfigExtra,
});
// HelloWorld.ts
const theme: AppConfigExtra['theme'] = Constants.expoConfig?.extra?.theme;⚠️
It is only safe to use JSON values in the extra config!
By adding fields to the extra property like this, you can extend your flavors to include any JSON value. Just keep in mind that not everything needs or should be handled at prebuild time, and sometimes it makes more sense to create a different architecture around your specific needs.
This is just meant as a showcase to demonstrate what is possible and open your mind to having concerns that live on the level of app flavors also be encapsulated on that level, keeping the rest of the code more streamlined in the process.
Country/Region flavors
Depending on the features of your app, you might require a separation of your app for legal reasons between e.g. the US and the EU markets, such as GDPR compliance or payment/tax concerns. With the tools shown in this blog post you can easily connect to different APIs and enable features in a region specific manner.
Subscription presets
When working with an app, that has different subscription models, having flavors for debugging each of them individually might be helpful. You could e.g. have a free, basic and pro variant ready to be built at any time without going through cumbersome account setup for yourself or other people testing it.
Seasonal Splash Screens
Maybe your app has some sort of recurring seasonal event, that deserves its own splash screen and app icon. Instead of removing the old assets and adding the new ones whenever there is a season transition, you can keep them permanently and just build the corresponding flavors for e.g. summer and new year releases.
What not to use flavors for
As you have seen in the post so far, app flavors are quite flexible and powerful. But with great power comes…. the possibility to misuse it for less appropriate means. Like e.g. throwing every single local state into a global redux store, just because you can. So always take the time to think if app flavors are the best tool in your toolbelt to meet the requirements you have at hand.
Here are a couple of examples where app flavors are not a good fit.
Flavors are not designed to be a replacement for a feature flag system
While we mentioned enabling features in a region specific manner in one of the previous examples, this is more tailored to e.g. disabling location services entirely and removing the related permission from the resulting app manifest.
Because of this they are also not a good fit for A/B-testing.
Flavors are not for having user dependent differences
Different flavors mean different binaries. For them to be targeting or be used by specific users, means having them released as separate apps in the stores. If e.g. you have admin and regular users, that require a different subset of the apps functionality, it is usually better to have that covered through proper permission management and have the app decide at runtime what to offer the user.
Wrapping up
App flavors are a powerful tool that enable building app variants with only a small overhead.
It is flexible enough to be used simply to help your team with your processes in regards to testing and QA, while at the same time providing opportunities for managing build time concerns like building white labelling apps.
If you have experience utilising it other interesting ways, or ran into trouble implementing it yourself, feel free to reach out to us. We are happy to help!