Why EAS Build Changes Everything
Historically, deploying a React Native app required a Mac for iOS builds, Android Studio for Play Store submissions, and careful management of signing certificates that were easy to lose and hard to regenerate. Expo Application Services (EAS) moves all of this to the cloud. You run eas build, Expo's servers build the native binary, and you submit it to the stores — without a Mac, without Android Studio, and with signing credentials managed securely in Expo's infrastructure.
EAS Build Configuration
EAS uses eas.json to define build profiles. You typically need three: development (for device testing with Expo Dev Client), preview (internal distribution), and production (store submission).
{
"cli": { "version": ">= 10.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": false },
"android": { "buildType": "apk" }
},
"preview": {
"distribution": "internal",
"ios": { "enterpriseProvisioning": "adhoc" },
"android": { "buildType": "apk" }
},
"production": {
"autoIncrement": true,
"ios": { "buildConfiguration": "Release" },
"android": { "buildType": "aab" }
}
},
"submit": {
"production": {
"ios": {
"appleId": "your@apple.id",
"ascAppId": "1234567890"
},
"android": {
"serviceAccountKeyPath": "./google-service-account.json",
"track": "internal"
}
}
}
}
Environment Configuration for Multiple Environments
Use app.config.ts (dynamic config) instead of app.json to inject environment-specific values. Store secrets in EAS secrets, not in the repo.
// app.config.ts
export default ({ config }) => ({
...config,
name: process.env.APP_ENV === 'production' ? 'MyApp' : 'MyApp (Dev)',
ios: {
bundleIdentifier: process.env.APP_ENV === 'production'
? 'com.company.myapp'
: 'com.company.myapp.dev',
},
android: {
package: process.env.APP_ENV === 'production'
? 'com.company.myapp'
: 'com.company.myapp.dev',
},
extra: {
apiUrl: process.env.API_URL,
eas: { projectId: 'your-project-id' },
},
})
// Set secrets in EAS (not in .env files)
// eas secret:create --scope project --name API_URL --value https://api.prod.com
Over-the-Air Updates with Expo Updates
EAS Update lets you push JavaScript changes to production users without going through the App Store review process. This is the fastest way to deploy bug fixes — updates are downloaded in the background and applied on the next app launch.
# Push an update to production channel
eas update --branch production --message "Fix checkout crash"
# In your app — check for updates on launch
import * as Updates from 'expo-updates'
async function checkForUpdates() {
if (!__DEV__) {
const update = await Updates.checkForUpdateAsync()
if (update.isAvailable) {
await Updates.fetchUpdateAsync()
await Updates.reloadAsync()
}
}
}
OTA updates only work for JavaScript changes. Any change to native code (adding a new native module, changing permissions) requires a full store build and submission.
CI/CD with GitHub Actions
name: EAS Build and Submit
on:
push:
branches: [main]
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Build production
run: eas build --platform all --profile production --non-interactive
- name: Submit to stores
run: eas submit --platform all --latest --non-interactive
if: startsWith(github.ref, 'refs/tags/v')
Common Deployment Issues
- Signing certificate mismatch: Let EAS manage certificates. If you manage them manually, keep them in 1Password or AWS Secrets Manager — losing an iOS distribution certificate means re-signing with a new one and losing push notification history.
- Build number not incrementing: Set
autoIncrement: trueineas.json— stores reject builds with the same build number. - OTA update not applying: Check that the runtime version in the update matches the installed app's runtime version. A mismatch silently skips the update.
- App Store rejection for privacy: All third-party SDKs that access device data must have usage description strings in
Info.plist. Missing strings are the #1 cause of App Store rejections.