Expo EAS Build: Cloud Compilation for React Native
Expo’s EAS (Expo Application Services) Build transforms the way you compile React Native apps by moving the heavy lifting to the cloud. No more fiddling with local Xcode or Android Studio installations, no more “it works on my machine” headaches. In this article we’ll walk through the entire workflow—from setting up a project, configuring build profiles, to deploying a production‑ready binary—all while keeping the focus on real‑world scenarios you’ll encounter daily.
What Is EAS Build?
EAS Build is a managed CI/CD service that runs your React Native build on powerful, pre‑configured macOS and Linux machines. It supports both managed and bare workflows, meaning you can stay within the Expo ecosystem or drop down to native code when needed. The service abstracts away platform‑specific quirks, automatically handles SDK version compatibility, and gives you granular control via eas.json profiles.
Because the compilation happens in the cloud, you can trigger builds from any OS—Windows, Linux, or macOS—without worrying about installing the correct version of the Android NDK or CocoaPods. The result is a reproducible, consistent binary that matches the exact configuration you defined in your repo.
Setting Up Your Project for EAS
Before you can start building, make sure your project is using an Expo SDK that supports EAS (SDK 45+ is safe). If you’re on a managed workflow, run:
expo install expo-cli
expo upgrade
Next, install the EAS CLI globally and log in to your Expo account:
npm install -g eas-cli
eas login
Once authenticated, initialize the EAS configuration. This creates an eas.json file at the root of your project:
eas init
Project Structure Overview
app.json/app.config.js– Global Expo configuration (app name, version, icons).eas.json– Build profiles for different environments (development, preview, production).metro.config.js– Optional Metro bundler tweaks.android/andios/– Native directories (present in bare or when you runeas eject).
Configuring eas.json
The heart of EAS Build lies in the eas.json file. It lets you define multiple build profiles, each with its own set of flags, environment variables, and credential handling. Below is a typical configuration for a project that targets both Android and iOS:
{
"cli": {
"version": ">= 3.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"android": {
"gradleCommand": ":app:assembleDebug"
},
"ios": {
"simulator": true
}
},
"preview": {
"distribution": "internal",
"env": {
"API_URL": "https://api.staging.example.com"
},
"android": {
"gradleCommand": ":app:assembleRelease"
},
"ios": {
"buildConfiguration": "Release"
}
},
"production": {
"distribution": "store",
"android": {
"gradleCommand": ":app:assembleRelease"
},
"ios": {
"buildConfiguration": "Release"
},
"env": {
"API_URL": "https://api.example.com"
}
}
}
}
Notice how each profile can set its own env block. These variables are injected at build time, allowing you to swap API endpoints, feature flags, or any secret without touching the source code.
Customizing Gradle and Xcode Settings
- Gradle command: Use
gradleCommandto run a specific Gradle task. For example,:app:bundleReleaseJsAndAssetsbundles JS before the native build. - Xcode build configuration: The
buildConfigurationkey maps to Xcode schemes (Debug/Release). You can also reference a custom scheme defined in Xcode.
Building for Android
To trigger an Android build, run the CLI with the desired profile. For a production release:
eas build --platform android --profile production
EAS will spin up a Linux builder, install the correct JDK, Android SDK, and run the Gradle command you specified. While the build is in progress, you can monitor its status in the terminal or visit the Expo dashboard for a visual log.
When the build finishes, you’ll receive an .aab (Android App Bundle) and an .apk (for testing). The bundle is the preferred artifact for Google Play because it enables dynamic delivery of assets per device.
Signing the Android App
EAS can manage your keystore automatically. Run:
eas credentials --platform android
If you already have a keystore, upload it; otherwise, let EAS generate one. The service stores the keystore securely and injects it during the build, removing the need for local key management.
Building for iOS
iOS builds require a macOS builder, which EAS provides out of the box. Initiate a production build with:
eas build --platform ios --profile production
The builder will install the correct Xcode version, CocoaPods, and run xcodebuild with the configuration you defined. If you’re on a managed workflow, the resulting artifact is an .ipa ready for TestFlight or App Store submission.
Just like Android, EAS can handle your provisioning profiles and distribution certificates. Use the credentials command to either upload existing ones or let EAS generate them for you:
eas credentials --platform ios
Enabling App Store Connect Integration
After a successful build, you can automatically upload the .ipa to App Store Connect:
eas submit --platform ios --profile production
This command pulls the latest build, signs it (if needed), and creates a new version in App Store Connect, streamlining the release pipeline.
Using Environment Variables Securely
Hard‑coding API keys or secrets in your source code is a recipe for disaster. EAS lets you inject variables at build time, but you should also protect them from being exposed in the final bundle.
One common pattern is to read variables from process.env in a separate config file, then replace them with static values only in production builds. Here’s a minimal example:
// config.js
const ENV = {
API_URL: process.env.API_URL,
SENTRY_DSN: process.env.SENTRY_DSN,
};
export default ENV;
When you run eas build --profile production, the API_URL and SENTRY_DSN values from eas.json are injected, and the JavaScript bundle contains the resolved strings. For added security, you can also enable the expo-constants “runtime version” feature to tie the bundle to a specific native version.
Optimizing Build Times
Even though the compilation happens in the cloud, you can still shave minutes off each run by caching dependencies and reusing previous artifacts. EAS automatically caches node_modules, Gradle, and CocoaPods between builds, but you can fine‑tune the behavior.
- Cache keys: Define a
cachesection ineas.jsonto specify additional paths, such as a custommetro.config.jsthat pulls in heavy assets. - Incremental builds: For Android, use the
--no-restore-cacheflag only when you need a clean slate (e.g., after updating native modules). - Parallel uploads: When submitting to multiple stores, run
eas submitin parallel processes to avoid sequential bottlenecks.
Pro tip: Enable “EAS Build Insights” in the Expo dashboard. It gives you a breakdown of each step’s duration, helping you pinpoint slow phases like Gradle dependency resolution or Xcode code signing.
Real‑World Use Cases
1. Continuous Integration for a SaaS Mobile App
A startup needed nightly builds for Android and iOS to run automated UI tests on BrowserStack. By adding a GitHub Actions workflow that runs eas build with the preview profile, they eliminated local build servers entirely. The workflow also uploaded the resulting artifacts to a private S3 bucket, where the test suite fetched them.
2. Multi‑Flavor Android Builds
A client required separate “free” and “premium” flavors. Using Gradle product flavors, they added two entries under the android block in eas.json:
"android": {
"free": {
"gradleCommand": ":app:assembleFreeRelease"
},
"premium": {
"gradleCommand": ":app:assemblePremiumRelease"
}
}
Running eas build --profile production --platform android --profile free generated the free flavor, while the premium build used the other command. No extra CI configuration was needed.
3. OTA Updates with EAS Submit
After publishing a new version to the Play Store, the team leveraged EAS’s OTA (over‑the‑air) capabilities. They ran eas update --branch production to push JavaScript changes instantly, while the native binary remained untouched. This decouples UI tweaks from full store releases, dramatically reducing time‑to‑market.
Advanced: Custom Build Scripts
Sometimes you need to run a script before the native build—think generating a version file, injecting a feature flag, or compressing assets. EAS supports a preBuild hook that runs on the builder machine.
{
"build": {
"production": {
"preBuild": "node scripts/generateVersion.js",
"android": {
"gradleCommand": ":app:assembleRelease"
}
}
}
}
Here’s a simple generateVersion.js that writes the Git SHA into a JSON file accessible at runtime:
// scripts/generateVersion.js
const { execSync } = require('child_process');
const fs = require('fs');
const sha = execSync('git rev-parse --short HEAD')
.toString()
.trim();
const versionInfo = {
gitSha: sha,
buildDate: new Date().toISOString(),
};
fs.writeFileSync('assets/version.json', JSON.stringify(versionInfo));
console.log('✅ version.json generated');
Now you can read version.json inside your app and display the current build hash, which is invaluable for debugging production issues.
Managing Credentials Safely
EAS stores all signing credentials in an encrypted vault tied to your Expo account. You can view, rotate, or revoke them via the eas credentials command or the web dashboard. Never commit keystores, provisioning profiles, or certificates to your repository.
For teams with strict compliance requirements, enable “Two‑Factor Authentication” on your Expo account and restrict access to the credentials page using organization roles. This ensures only authorized members can modify signing material.
Pro tip: When using a CI system, generate a short‑lived “Expo access token” (via eas login --token) and store it as a secret in your CI environment. This avoids persisting long‑lived credentials in the pipeline.
Monitoring Build Health
EAS provides a dashboard that aggregates logs, build times, and failure reasons. Use the “Artifacts” tab to download the exact binary that was produced, which is handy for reproducing bugs reported by QA.
For automated monitoring, you can subscribe to webhook events. When a build finishes, EAS can POST a JSON payload to a Slack channel or a custom endpoint, enabling you to trigger downstream actions like notifying stakeholders or updating a release changelog.
Best Practices Checklist
- Keep
eas.jsonunder version control and avoid committing secrets. - Use separate profiles for development, preview, and production to isolate environment variables.
- Leverage EAS’s credential manager; never store keystores in the repo.
- Enable caching and monitor build insights to reduce CI costs.
- Automate post‑build steps (e.g., upload to S3, notify Slack) via webhooks.
- Regularly rotate API keys and update the corresponding
enventries.
Conclusion
Expo EAS Build bridges the gap between the simplicity of managed Expo projects and the power of native builds. By offloading compilation to the cloud, you gain reproducible binaries, faster onboarding for new developers, and a streamlined path from code to store. Whether you’re a solo developer pushing nightly updates or a large team orchestrating multi‑flavor releases, EAS provides the flexibility, security, and speed needed for modern React Native development. Start experimenting with the free tier, integrate it into your CI pipeline, and watch your build times shrink while your confidence in release quality soars.