EAS -- Build + Submit + Update
Expo Application Services. Builds, submits, and updates your app.
eas.json
{
"cli": { "version": ">= 12.0.0", "appVersionSource": "remote" },
"build": {
"base": {
"node": "22.0.0",
"env": {
"EXPO_PUBLIC_API_URL": "https://api.example.com"
}
},
"development": {
"extends": "base",
"developmentClient": true,
"distribution": "internal",
"env": { "EXPO_PUBLIC_API_URL": "https://dev-api.example.com" },
"ios": { "simulator": true }
},
"preview": {
"extends": "base",
"distribution": "internal",
"channel": "preview"
},
"production": {
"extends": "base",
"autoIncrement": true,
"channel": "production"
}
},
"submit": {
"production": {
"ios": { "appleId": "dev@example.com", "ascAppId": "1234567890" },
"android": { "serviceAccountKeyPath": "./service-account.json", "track": "internal" }
}
}
}
Build Commands
# Development build (custom dev client)
bunx eas build --profile development --platform ios
bunx eas build --profile development --platform android
# Preview (internal distribution)
bunx eas build --profile preview --platform all
# Production
bunx eas build --profile production --platform all
# Local build (no EAS servers)
bunx eas build --profile development --platform ios --local
OTA Updates
# Publish update to a channel
bunx eas update --channel preview --message "Fix login crash"
bunx eas update --channel production --message "v1.2.3 release"
# Check update status
bunx eas update:list
app.json update config:
{
"expo": {
"updates": {
"url": "https://u.expo.dev/YOUR-PROJECT-ID",
"enabled": true,
"fallbackToCacheTimeout": 0,
"checkAutomatically": "ON_LOAD"
},
"runtimeVersion": { "policy": "appVersion" }
}
}
Handle updates in root _layout.tsx:
import * as Updates from 'expo-updates'
import { useEffect } from 'react'
export default function RootLayout() {
// OTA update check is an SDK imperative call, not server data fetching --
// React Query doesn't apply here. This is the only acceptable useEffect+async pattern.
useEffect(() => {
async function checkUpdate() {
if (__DEV__) return
const update = await Updates.checkForUpdateAsync()
if (update.isAvailable) {
await Updates.fetchUpdateAsync()
await Updates.reloadAsync()
}
}
checkUpdate()
}, [])
// ...
}
Submit to Stores
# After a production build
bunx eas submit --platform ios --latest
bunx eas submit --platform android --latest
# Submit specific build
bunx eas submit --platform ios --id BUILD_ID
EAS Secrets
# Set secrets (never commit these)
bunx eas secret:create --scope project --name API_SECRET_KEY --value "..."
bunx eas secret:create --scope project --name SENTRY_DSN --value "..."
# List secrets
bunx eas secret:list
Access in build via eas.json env:
{ "build": { "production": { "env": { "SENTRY_DSN": "$SENTRY_DSN" } } } }
GitHub Actions CI
.github/workflows/eas-build.yml:
name: EAS Build
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- run: bunx eas build --profile production --platform all --non-interactive
.github/workflows/eas-update.yml:
name: EAS Update
on:
push:
branches: [main]
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- uses: expo/expo-github-action@v8
with: { eas-version: latest, token: '${{ secrets.EXPO_TOKEN }}' }
- run: bunx eas update --channel production --message "${{ github.event.head_commit.message }}" --non-interactive
Rules
eas.jsonmust havedevelopment,preview, andproductionprofiles- Never hardcode API URLs in
eas.json-- use EAS secrets or env vars - Never commit
service-account.jsonor Apple credentials -- use EAS secrets concurrency.cancel-in-progress: falsein CI -- never cancel in-flight buildsruntimeVersionpolicy must be consistent across OTA updates and builds- Always test OTA update compatibility before shipping to production