Build Profiles & Flavors
Dev, preview, production, and white-label variants - the cookbook for structuring eas.json profiles and dynamic app.config.ts on Expo SDK 57.
Search across all documentation pages
Dev, preview, production, and white-label variants - the cookbook for structuring eas.json profiles and dynamic app.config.ts on Expo SDK 57.
Quick-reference recipe card - copy-paste ready.
// eas.json - three-environment baseline
{
"cli": { "version": ">= 16.0.0", "appVersionSource": "remote" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"environment": "development",
"channel": "development"
},
"preview": {
"distribution": "internal",
"environment": "preview",
"channel": "preview"
},
"production": {
"distribution": "store",
"environment": "production",
"channel": "production",
"autoIncrement": true
}
}
}// app.config.ts - APP_VARIANT from eas.json env
import type { ExpoConfig } from "expo/config";
const variant = process.env.APP_VARIANT ?? "production";
const bundleIds: Record<string, string> = {
development: "com.example.shopapp.dev",
preview: "com.example.shopapp.staging",
production: "com.example.shopapp",
};
const config: ExpoConfig = {
name: variant === "production" ? "Shop" : `Shop (${variant})`,
slug: "shop-app",
ios: { bundleIdentifier: bundleIds[variant] },
android: { package: bundleIds[variant] },
extra: {
apiUrl: process.env.EXPO_PUBLIC_API_URL,
eas: { projectId: "00000000-0000-0000-0000-000000000000" },
},
};
export default config;// eas.json - pass variant into prebuild
{
"build": {
"preview": {
"env": { "APP_VARIANT": "preview" },
"environment": "preview"
}
}
}When to reach for this:
Step 1 - Define profile responsibilities
| Profile | Audience | Distribution | Typical binary |
|---|---|---|---|
| development | Engineers | internal | Dev client + simulator/APK |
| preview | QA / stakeholders | internal | Release-shaped IPA/APK |
| production | End users | store | AAB + App Store IPA |
eas build --profile development --platform all
eas build --profile preview --platform ios
eas build --profile production --platform androidStep 2 - Bind EAS environments
Create variables in Expo dashboard → Project → Environment variables:
development EXPO_PUBLIC_API_URL=https://api.dev.example.com
preview EXPO_PUBLIC_API_URL=https://api.staging.example.com
production EXPO_PUBLIC_API_URL=https://api.example.com{
"build": {
"preview": { "environment": "preview" }
}
}environment pulls vars from EAS - not laptop .env.localeas update --environment production must match production profile - Release & OTA RulesStep 3 - White-label with app.config.ts
// app.config.ts - multi-brand from BRAND env
type Brand = "acme" | "contoso";
const brand = (process.env.BRAND ?? "acme") as Brand;
const brands: Record<Brand, { name: string; bundleId: string; icon: string }> = {
acme: {
name: "Acme Shop",
bundleId: "com.acme.shop",
icon: "./brands/acme/icon.png",
},
contoso: {
name: "Contoso Market",
bundleId: "com.contoso.market",
icon: "./brands/contoso/icon.png",
},
};
const config: ExpoConfig = {
name: brands[brand].name,
slug: `shop-${brand}`,
icon: brands[brand].icon,
ios: { bundleIdentifier: brands[brand].bundleId },
android: {
package: brands[brand].bundleId,
adaptiveIcon: { foregroundImage: brands[brand].icon, backgroundColor: "#fff" },
},
};
export default config;// eas.json - one profile per brand for production
{
"build": {
"production-acme": {
"extends": "production",
"env": { "BRAND": "acme" },
"environment": "production-acme"
},
"production-contoso": {
"extends": "production",
"env": { "BRAND": "contoso" },
"environment": "production-contoso"
}
}
}eas build --profile production-acme --platform all
eas build --profile production-contoso --platform allprojectId or careful channel isolation per bundle IDbundleIdentifier / packageDRY shared settings across profiles:
{
"build": {
"base": {
"node": "22.11.0",
"resourceClass": "medium"
},
"development": {
"extends": "base",
"developmentClient": true,
"distribution": "internal"
},
"production": {
"extends": "base",
"distribution": "store",
"autoIncrement": true
},
"production-ios-simulator": {
"extends": "production",
"ios": { "simulator": true },
"distribution": "internal"
}
}
}extends merges parent keys - child overrides winExpo CNG typically uses one Gradle applicationId per prebuild driven by app.config.ts:
eas.json profile → APP_VARIANT env → app.config.ts → applicationIdNative Android product flavors in build.gradle are possible in brownfield projects but fight CNG regeneration. Prefer:
app.config.ts for bundle ID, name, and iconsSee Prebuild Basics - hand-edited android/app/build.gradle product flavors are lost on clean prebuild.
// app.config.ts
runtimeVersion: {
policy: "fingerprint",
},
updates: {
url: "https://u.expo.dev/<project-id>",
},{
"build": {
"preview": { "channel": "preview" },
"production": { "channel": "production" }
}
}eas update --channel preview --environment preview --message "QA fix"
eas update --channel production --environment production --message "Hotfix"eas updatebundleIdentifier / package differ - use APP_VARIANT suffixes like .dev and .staging.extends when needed."jane-debug") - use environments and channels instead.BRAND env + separate credentials per package name.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 19, 2026