without your own Mac, without local toolchain drift
EAS Build compiles React Native and Expo apps in the cloud into installable iOS and Android binaries, manages certificates, provisioning profiles, and keystores automatically, and makes builds reproducible through eas.json. This guide explains build profiles, environment variables, config plugins for native modules, and the path from build to submission with eas submit.
Table of Contents
- 1. The real problem EAS Build solves
- 2. eas.json: build profiles for development, preview, and production
- 3. Environment variables and secrets per build profile
- 4. Credentials management: certificates and keystores automated
- 5. Native modules and config plugins in the cloud
- 6. Build triggers: local eas build versus a CI pipeline
- 7. From build to store: eas submit
- 8. Why EAS Build replaces classic Expo Build and the bare Xcode workflow
- 9. Bare workflow, classic Expo Build, and EAS Build compared
- 10. Summary
- 11. FAQ
1. The real problem EAS Build solves
EAS Build is Expo's cloud build service for React Native apps, and it solves a problem every team eventually runs into: iOS builds strictly require a Mac with Xcode installed, while Android builds need a maintained Android Studio and SDK installation. For a React Native team without its own pool of Mac hardware, that used to mean either an expensive Mac Mini farm in the office or no independent iOS releases at all. EAS Build moves the entire compilation step onto Expo's infrastructure, so a developer on a Windows or Linux machine can trigger a full iOS build without ever touching a Mac.
The second real pain point EAS Build addresses is setup drift between team members. With local builds, every developer potentially has a different Xcode version, a different CocoaPods version, a different Android NDK version installed, and a build that works on one machine fails on another with cryptic native error messages. EAS Build runs every build inside a defined, versioned build image, so the same combination of Xcode version, Node version, and native toolchains is used for every build, regardless of which developer triggered it.
The third advantage concerns reproducibility in CI environments. Without EAS Build, a team would have to operate its own Mac runners for GitHub Actions or GitLab CI, including maintenance, updates, and capacity planning. With EAS Build, CI configuration shrinks down to an API call against the EAS infrastructure, letting React Native projects use the same build pipeline for local development, pull request checks, and store releases, without running their own build servers.
2. eas.json: build profiles for development, preview, and production
The central configuration file for EAS Build is eas.json at the project root. It defines named build profiles, each describing its own combination of distribution method, native build settings, and environment variables. The three profiles found in nearly every React Native project are development, preview, and production: development for builds with an embedded dev client and debug menu, preview for internal test builds that can be installed directly, and production for the final store build.
One decisive detail in eas.json is the distribution field. The value internal produces a build that can be downloaded and installed directly, for example via a QR code sent to registered test devices, ideal for preview builds that skip TestFlight or an internal Play Store test track entirely. The value store instead produces an artifact in the format App Store Connect or the Play Console expects, meaning a signed .ipa or an Android App Bundle. This separation lets the same project and the same EAS Build configuration produce both fast internal test builds and final store builds, without manually switching build settings back and forth.
For Android, each profile can additionally control the output type via buildType: apk for easily installable test builds, app-bundle for Play Store submission. For iOS, simulator controls whether a build compiles for the iOS simulator instead of real devices, which is useful for automated UI tests in CI without a physical test device. All three profiles live side by side in the same eas.json, and every EAS Build invocation explicitly selects which configuration to use via the --profile flag.
{
"cli": {
"version": ">= 13.0.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": true },
"android": { "buildType": "apk" },
"env": { "APP_ENV": "development" }
},
"preview": {
"distribution": "internal",
"channel": "preview",
"ios": { "simulator": false },
"android": { "buildType": "apk" },
"env": { "APP_ENV": "preview" }
},
"production": {
"distribution": "store",
"channel": "production",
"autoIncrement": true,
"android": { "buildType": "app-bundle" },
"env": { "APP_ENV": "production" }
}
},
"submit": {
"production": {}
}
}
3. Environment variables and secrets per build profile
Every build profile in eas.json can define its own env block, exposed as environment variables inside the build environment. For React Native and Expo apps, this is the central mechanism for distinguishing API endpoints, feature flags, or bundle identifiers between development, preview, and production, without branching the application code itself. In app.config.js, the value is typically read via process.env.APP_ENV and decides which API base URL, app name, or app icon flows into that particular build.
Not every environment variable belongs in eas.json itself, since this file is usually committed to the Git repository. For sensitive values like API keys, Sentry DSN tokens, or third-party service credentials, EAS Build offers a secrets store: eas secret:create encrypts a value and stores it with Expo, making it available in the build under the same variable name, for example as process.env.SENTRY_AUTH_TOKEN, without the plaintext value ever appearing in the repository or in eas.json. For teams that maintain the same configuration logic for CI outside of EAS, this is a cleaner alternative to secrets scattered across a particular CI provider's pipeline variables.
It is important to distinguish between build-time and runtime variables. Values set via env in eas.json or via eas secret:create are available at the time the EAS Build run executes and get baked into the JavaScript code during bundling, so a later change requires a new build, not just a runtime config refresh. Anyone who instead wants to change values at runtime without triggering a new build needs a separate remote config system, or eas update for pure JavaScript and asset changes to an already-built binary. EAS Build produces exactly the binary that eas update later delivers patches to; the two services are deliberately separate steps in the same release chain.
# Log in once per machine or CI runner
eas login
# Store a sensitive value encrypted - never lands in eas.json or git
eas secret:create --scope project --name SENTRY_AUTH_TOKEN --value "xxxxx" --type string
# Trigger a cloud build for a specific profile and platform
eas build --profile development --platform ios
eas build --profile preview --platform android
# Build both platforms for the store release, non-interactive for CI
eas build --profile production --platform all --non-interactive
# Inspect recent EAS Build runs and their status
eas build:list --limit 10
4. Credentials management: certificates and keystores automated
One of the biggest practical advantages of EAS Build is the automatic management of iOS and Android signing material directly inside the build flow. When an iOS build runs with distribution store and credentialsSource remote, EAS Build first checks whether a valid distribution certificate and a matching provisioning profile already exist for the configured bundle ID. If they are missing, the interactive flow offers to generate them on the spot and store them encrypted in Expo's infrastructure, so a developer never has to log into the Apple Developer Portal or export a certificate by hand.
For Android, EAS Build plays the same role for keystores: if no keystore exists yet for the project, one is generated automatically on the first production build and permanently linked to the Expo project, so every subsequent build gets signed with the same key, a requirement for the Play Store to accept updates to the same app at all. Existing keystores or iOS certificates can be imported at any time, in case a team already owned its own signing material before switching to EAS Build, for example from an earlier Fastlane or manual Xcode workflow.
Important for day-to-day build operations: the credentialsSource field in eas.json decides per profile whether remote (the variant managed by EAS) or local (files provided locally, for example for teams running their own credentials repository) is used. A single project could set production to remote and a separate profile for enterprise distribution to local. The eas credentials command lets you inspect at any time which certificate, profile, or keystore is currently active for a build profile, so EAS Build remains no black box despite the automation.
5. Native modules and config plugins in the cloud
As soon as a React Native project pulls in native modules with their own native code, for example for Bluetooth communication, camera access, or push notifications, plain JavaScript is no longer enough to control the native configuration. Expo solves this with config plugins: small, declarative functions that, during the preceding prebuild step of EAS Build, modify the generated native projects, meaning the Xcode project and the Android Gradle project, before the actual native compiler starts. A config plugin can, for instance, add an entry to Info.plist, set a Gradle property, or register an additional native dependency.
The decisive advantage over manually editing the ios/ and android/ folders: in managed workflow projects, these folders do not permanently exist in the repository at all, they are freshly generated by EAS Build from app.json/app.config.js and the registered config plugins on every cloud build. That keeps the native configuration declarative, version-controlled, and reproducible; a manual edit to a generated Xcode project would just get overwritten on the next build anyway. For projects that need to permanently maintain their own native files, EAS Build also supports local prebuild output (npx expo prebuild) that then gets versioned and is no longer regenerated automatically.
In practice, this means a team pulling in react-native-ble-plx for Bluetooth Low Energy never has to touch Info.plist or build.gradle by hand. A config plugin writes the required Bluetooth permissions into Info.plist and, if needed, raises minSdkVersion in build.gradle, and EAS Build applies these changes automatically on every cloud build, whether the build was triggered locally or through CI.
// app.config.js: environment-specific values read at build time,
// resolved from the profile's env block in eas.json
const APP_ENV = process.env.APP_ENV || "development";
const variants = {
development: { name: "Mironsoft Demo (Dev)", bundleIdentifier: "de.mironsoft.demo.dev" },
preview: { name: "Mironsoft Demo (Preview)", bundleIdentifier: "de.mironsoft.demo.preview" },
production: { name: "Mironsoft Demo", bundleIdentifier: "de.mironsoft.demo" },
};
export default ({ config }) => ({
...config,
name: variants[APP_ENV].name,
ios: { ...config.ios, bundleIdentifier: variants[APP_ENV].bundleIdentifier },
android: { ...config.android, package: variants[APP_ENV].bundleIdentifier },
plugins: [
"expo-router",
["react-native-ble-plx", { isBackgroundEnabled: true }],
"./plugins/withCustomEntitlements",
],
extra: {
eas: { projectId: "00000000-0000-0000-0000-000000000000" },
},
});
A concrete example of the iOS side of a config plugin: instead of editing Info.plist by hand after every EAS Build prebuild, the withInfoPlist plugin writes additional keys, such as background modes or associated domains, directly into app.json. The following values land unchanged in the generated Info.plist of the native iOS project before the actual compiler run begins.
{
"expo": {
"ios": {
"bundleIdentifier": "de.mironsoft.demo",
"infoPlist": {
"UIBackgroundModes": ["fetch", "remote-notification"],
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": false
},
"com.apple.developer.associated-domains": ["applinks:mironsoft.de"]
},
"entitlements": {
"aps-environment": "production"
}
}
}
}
The same principle applies on the Gradle side for Android. If a native module, say a Bluetooth library, requires a higher minSdkVersion or additional ABI filters, a config plugin injects the matching code block directly into the generated build.gradle via withAppBuildGradle. A developer never has to open this file for that purpose, since EAS Build regenerates it from app.json and the registered plugins on every cloud build anyway.
// android/app/build.gradle: generated during prebuild, values injected
// by a config plugin using withAppBuildGradle (not hand-edited)
android {
defaultConfig {
applicationId "de.mironsoft.demo"
// Bumped from the template default because a native BLE module
// requires BluetoothLeScanner APIs only available from API 21+
minSdkVersion 21
targetSdkVersion 34
ndk {
// Limit native library packaging to the ABIs EAS Build
// actually needs to test on, reducing final app size
abiFilters "arm64-v8a", "armeabi-v7a"
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
}
6. Build triggers: local eas build versus a CI pipeline
An EAS Build can be triggered in two fundamentally different ways. The local path is the interactive terminal command eas build --profile preview --platform ios, which a developer runs directly from the project directory. The source code gets compressed and uploaded to Expo's build servers, compiled there in an isolated environment, and the finished build link appears in the terminal and in the Expo dashboard once it completes. This path works great for quick, ad hoc test builds during active development.
The second path wires EAS Build into a CI pipeline, for example GitHub Actions or GitLab CI. Instead of an interactive terminal invocation, the same eas build command runs with the --non-interactive flag inside a CI job, triggered by a git push, a merge into the main branch, or a manual workflow dispatch. The CI runner itself does not need to install any native toolchain at all; it simply invokes the EAS CLI, which delegates the actual build job to the EAS infrastructure and waits for the result. That reduces CI configuration to a handful of lines, regardless of how complex the React Native project's own native configuration is.
For teams using both paths in parallel, a clear convention helps: development and preview builds are usually triggered locally by developers as needed, while production builds run exclusively through the CI pipeline, so every store release is traceably tied to a specific commit and an automated, logged EAS Build run, instead of a manual action on a single developer's machine.
7. From build to store: eas submit
Once a production build with distribution store completes successfully, eas submit takes over the final step: uploading the finished .ipa or .aab artifact to App Store Connect or the Google Play Console. Without EAS Build and eas submit, this step would either have to happen manually through Xcode or the Play Console web interface, or through a separate tool like Fastlane deliver, which needs its own credentials and its own configuration.
The submit block in eas.json is configured per profile and typically references an App Store Connect API key for iOS or a service account JSON for Android, both stored securely as an EAS secret instead of in plaintext in the repository. That lets eas submit --platform ios --latest run right after a successful production build and automatically submit the most recently produced build, without manually looking up the build ID. For Android, eas submit can additionally specify which Play Console test track the build moves into, for example internal testing or production with a staged rollout percentage.
The practical effect: a complete release cycle, from a git tag through an automated EAS Build run all the way to submission at Apple and Google, can be represented as a single CI pipeline, without anyone having to manually download an artifact and then upload it again. The actual metadata upkeep in the App Store Connect listing itself, meaning screenshots, description copy, and privacy declarations, remains untouched by this and stays a separate, usually less frequently repeated task.
8. Why EAS Build replaces classic Expo Build and the bare Xcode workflow
Before EAS Build was introduced, Expo offered an older, now discontinued service called classic Expo Build (expo build:ios / expo build:android). That service was heavily limited: it supported no config plugin system for native modules, no named build profiles, and credentials were managed through a less transparent process that partly required manual confirmation. As soon as a project needed native code outside the Expo APIs supported at the time, teams had to switch entirely to the bare workflow with local Xcode and Android Studio builds, a break that forced many teams to maintain two completely different build systems.
The bare workflow approach with purely local builds does work for React Native apps with arbitrary native code, but it shifts every one of the challenges described earlier back onto the team: every developer needs a Mac for iOS builds, every toolchain version has to be kept in sync, and CI pipelines need their own maintained Mac runners. EAS Build closes exactly that gap by combining config plugins, named build profiles, and automated credentials management with full support for arbitrary native code, so even projects with extensive custom native modules now run through the same cloud build service, without having to fall back to a purely local workflow.
9. Bare workflow, classic Expo Build, and EAS Build compared
The following overview puts the three approaches a React Native team has historically had, or currently has, side by side for iOS and Android builds. It shows why EAS Build has become the default choice for most new Expo projects.
| Criterion | Bare workflow (local) | Classic Expo Build (discontinued) | EAS Build |
|---|---|---|---|
| Mac required for iOS builds | Yes, mandatory per developer | No | No |
| Setup time for a new team member | Hours to days (Xcode, SDKs) | Minutes | Minutes |
| Credentials management | Fully manual | Partly automated, not very transparent | Automated, inspectable via eas credentials |
| Native modules / custom native code | Fully supported | Not supported | Fully supported via config plugins and prebuild |
| CI integration | Requires your own Mac runner | Limited API | Native CLI integration, no build servers of your own |
| Current support status | Still possible, but maintenance-heavy | Discontinued | Actively developed |
In practice, this comparison means: the bare workflow with purely local builds remains a valid option for teams with very specific, unusual native requirements and existing Mac infrastructure, but it comes with ongoing maintenance overhead. Classic Expo Build, as a discontinued service, is no longer an option for new projects. EAS Build combines the flexibility of the bare workflow with the automation that earlier managed workflow users already knew from classic Expo Build, which makes it the most pragmatic starting point for almost any new React Native and Expo project.
Mironsoft
React Native development, cloud build pipelines, and app distribution
Ready to set up EAS Build for your React Native project?
We configure eas.json with clean build profiles for development, preview, and production, set up credentials and secrets securely, and wire EAS Build and eas submit into your CI pipeline.
eas.json setup
Structure build profiles, environment variables, and secrets cleanly
Migration
Move bare workflow or classic build projects onto EAS Build
CI integration
Wire eas build and eas submit into GitHub Actions or GitLab CI
10. Summary
EAS Build moves the entire iOS and Android compilation step for React Native and Expo apps into the cloud, removing the three biggest practical hurdles of local builds: the Mac requirement for iOS, setup drift between developer machines, and the effort of running your own CI build servers. Through eas.json, named build profiles for development, preview, and production can be defined with their own environment variables, their own distribution method, and their own native settings, while EAS secrets keep sensitive values encrypted and outside the repository.
iOS certificates, provisioning profiles, and Android keystores are generated, renewed, and managed automatically by EAS Build, yet remain inspectable and exportable at any time via eas credentials. Config plugins translate native requirements declaratively into generated Xcode and Gradle projects, so even projects with custom native code run entirely through the same cloud build service. With eas submit, the pipeline ends right at App Store Connect and the Google Play Console, making it possible to represent a complete release cycle without a manual download and upload step.
EAS Build for React Native: the essentials at a glance
Build profiles
eas.json defines development, preview, and production with their own distribution, env variables, and native settings.
Credentials
iOS certificates, provisioning profiles, and Android keystores are generated and managed automatically, inspectable via eas credentials.
Config plugins
Native changes to Info.plist and build.gradle run declaratively through config plugins, no manual editing of generated projects.
eas submit
Uploads finished builds automatically to App Store Connect and Google Play, chainable right after a successful EAS Build.