Eas Build Ota Updates
Deploying React Native apps with EAS Build, app store submission, and OTA updates via EAS Update
You are an expert in deployment for building cross-platform mobile apps with React Native. ## Key Points - `"appVersion"`: Uses the `version` field. Bump it when native code changes. - `"nativeVersion"`: Uses `ios.buildNumber` / `android.versionCode`. Most granular. - `"fingerprint"`: Automatically hashes native dependencies. Best for Expo managed workflow. - Use the `fingerprint` runtime version policy in Expo managed projects. It automatically detects native changes and prevents incompatible OTA updates. - Always test OTA updates on the `preview` channel before pushing to `production`. - Use `eas secret` for API keys and credentials instead of `.env` files in CI. Secrets are injected at build time and never appear in logs. - Set `"autoIncrement": true` in eas.json and use `"appVersionSource": "remote"` so EAS manages build numbers. This avoids conflicts in team environments. - Implement staged rollouts for production OTA updates, starting at 10-25% and monitoring crash rates before full rollout. - Always include a meaningful `--message` with OTA updates for auditability. - **Forgetting to bump runtime version**: When using `appVersion` policy, failing to bump the version after native changes causes OTA updates to target incompatible builds. - **Large OTA bundles**: OTA updates download the entire JS bundle. Large bundles on slow connections lead to timeouts. Monitor bundle size with `npx expo export --dump-sourcemap`. - **Not testing the update flow**: Test `Updates.checkForUpdateAsync()` and `reloadAsync()` in preview builds. The update flow is invisible in development mode. ## Quick Example ```bash npm install -g eas-cli eas login eas init # Links project to your Expo account ``` ``` Channel Branch Purpose ────────────── ────────────── ──────────────────────── development development Dev builds, frequent updates preview preview QA/tester builds production production Live app store users ```
skilldb get react-native-skills/Eas Build Ota UpdatesFull skill: 381 linesEAS Build and OTA Updates — React Native
You are an expert in deployment for building cross-platform mobile apps with React Native.
Core Philosophy
The deployment pipeline for a React Native app has two fundamentally different tracks: native builds (compiled binaries submitted to app stores) and JavaScript updates (OTA bundles delivered without store review). Understanding the boundary between these two tracks is the single most important deployment concept. A native build includes the compiled native code, native modules, and a JavaScript bundle. An OTA update replaces only the JavaScript bundle. If you add a native module or change native configuration, you need a new native build. If you fix a bug in JavaScript logic or update a component, an OTA update suffices.
The runtime version is the contract between native builds and OTA updates. It answers: "is this JavaScript bundle compatible with that native binary?" When native code changes, the runtime version must change, or OTA updates targeting the old runtime will crash on the new binary (or vice versa). The fingerprint policy automates this by hashing native dependencies, making it impossible to accidentally push an incompatible OTA update. For teams managing runtime versions manually, forgetting to bump the version after a native change is the most dangerous deployment mistake.
Staged rollouts are not optional for production OTA updates. Unlike native builds that go through app store review and have a natural delay, OTA updates take effect within minutes for all users on the matching runtime version. A bug in an OTA update reaches your entire user base almost instantly. Rolling out to 10% of users first, monitoring crash rates and error logs, and then increasing to 100% over hours or days is the only responsible way to deploy OTA updates to production.
Anti-Patterns
-
Pushing OTA updates that require native code changes: Adding a new native module and pushing a JavaScript update that imports it will crash for users who have the old native build. The fingerprint runtime policy prevents this automatically, but custom policies require manual vigilance with every deployment.
-
Skipping the preview channel and pushing directly to production: Every OTA update should be tested on a preview build before it reaches production users. The cost of a brief delay is trivial compared to the cost of a bad update reaching all users simultaneously.
-
Committing secrets to app.config.ts or eas.json: API keys, service account credentials, and tokens in source control are accessible to anyone who clones the repository. Use
eas secretto store credentials that are injected at build time and never appear in logs or source code. -
Not passing --non-interactive in CI/CD pipelines: EAS commands prompt for user input by default. In a CI environment, this causes the build to hang indefinitely waiting for input that will never come. Always pass
--non-interactivein automated pipelines. -
Deploying large OTA bundles without monitoring bundle size: OTA updates download the entire JavaScript bundle. A 20MB bundle on a slow mobile connection may timeout, leaving the user with a stale version. Monitor bundle size with
npx expo export --dump-sourcemapand keep it lean by code-splitting and removing unused dependencies.
Overview
EAS (Expo Application Services) provides a complete deployment pipeline for React Native apps: EAS Build compiles native binaries in the cloud, EAS Submit sends them to app stores, and EAS Update delivers JavaScript bundle updates over the air (OTA) without a full app store review. This skill covers build profiles, CI/CD integration, update channels, and deployment strategies for production apps.
Core Concepts
EAS CLI Setup
npm install -g eas-cli
eas login
eas init # Links project to your Expo account
eas.json Build Profiles
{
"cli": {
"version": ">= 12.0.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
},
"env": {
"APP_VARIANT": "development"
},
"channel": "development"
},
"preview": {
"distribution": "internal",
"env": {
"APP_VARIANT": "preview"
},
"channel": "preview"
},
"production": {
"autoIncrement": true,
"env": {
"APP_VARIANT": "production"
},
"channel": "production"
}
},
"submit": {
"production": {
"ios": {
"appleId": "you@example.com",
"ascAppId": "1234567890",
"appleTeamId": "ABCDE12345"
},
"android": {
"serviceAccountKeyPath": "./google-service-account.json",
"track": "internal"
}
}
}
}
Build and Submit Commands
# Development build (with dev client)
eas build --profile development --platform ios
# Preview build (internal distribution for testers)
eas build --profile preview --platform all
# Production build
eas build --profile production --platform all
# Submit to stores after build
eas submit --profile production --platform ios
eas submit --profile production --platform android
# Build and submit in one command
eas build --profile production --platform all --auto-submit
EAS Update (OTA)
# Publish an update to the preview channel
eas update --channel preview --message "Fix login button alignment"
# Publish to production
eas update --channel production --message "Patch: handle null user gracefully"
# Publish targeting a specific branch
eas update --branch production --message "Hotfix: crash on empty list"
Implementation Patterns
Channel and Branch Strategy
Channel Branch Purpose
────────────── ────────────── ────────────────────────
development development Dev builds, frequent updates
preview preview QA/tester builds
production production Live app store users
# Map a channel to a branch
eas channel:edit production --branch production
eas channel:edit preview --branch preview
Runtime Version Policy
The runtime version determines which OTA updates are compatible with which native builds. If native code changes, the runtime version must change.
// app.json
{
"expo": {
"runtimeVersion": {
"policy": "appVersion"
}
}
}
Policies:
"appVersion": Uses theversionfield. Bump it when native code changes."nativeVersion": Usesios.buildNumber/android.versionCode. Most granular."fingerprint": Automatically hashes native dependencies. Best for Expo managed workflow.
{
"expo": {
"runtimeVersion": {
"policy": "fingerprint"
}
}
}
CI/CD with GitHub Actions
# .github/workflows/eas-build.yml
name: EAS Build & Update
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
update:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- run: eas update --channel production --message "${{ github.event.head_commit.message }}"
build:
if: github.event_name == 'push' && contains(github.event.head_commit.message, '[build]')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- run: eas build --profile production --platform all --non-interactive --auto-submit
preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- run: eas update --branch pr-${{ github.event.number }} --message "PR #${{ github.event.number }}"
Versioning Strategy
// app.config.ts
export default {
version: "2.3.0", // User-facing version (semver)
ios: {
buildNumber: "1", // Auto-incremented by EAS
},
android: {
versionCode: 1, // Auto-incremented by EAS
},
runtimeVersion: {
policy: "fingerprint",
},
};
# Let EAS manage build numbers
# In eas.json, set "autoIncrement": true
# EAS tracks the latest build number remotely
# Check current version
eas build:version:get --platform ios
eas build:version:get --platform android
# Manually set if needed
eas build:version:set --platform ios --build-number 42
Staged Rollout
# Create an update but only roll out to 10% of users
eas update --channel production --message "Test new feature" --rollout-percentage 10
# Check rollout status
eas update:list --channel production
# Increase rollout
eas channel:rollout production --percent 50
# Full rollout
eas channel:rollout production --percent 100
Environment Secrets
# Set secrets for EAS Build (not committed to source)
eas secret:create --scope project --name SENTRY_DSN --value "https://..."
eas secret:create --scope project --name API_KEY --value "sk_live_..."
# List secrets
eas secret:list
# Access in app.config.ts
export default {
extra: {
sentryDsn: process.env.SENTRY_DSN,
},
};
Pre-Install and Post-Install Hooks
// eas.json — custom build steps
{
"build": {
"production": {
"ios": {
"image": "latest",
"cocoapods": "1.15.2"
},
"android": {
"image": "latest",
"ndk": "25.1.8937393"
}
}
}
}
Monitoring Updates
// Check for updates on app launch
import * as Updates from "expo-updates";
async function checkForUpdates() {
if (__DEV__) return; // Skip in development
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
// Option 1: Immediate reload
await Updates.reloadAsync();
// Option 2: Prompt user
// showUpdatePrompt(() => Updates.reloadAsync());
}
} catch (error) {
console.error("Update check failed:", error);
}
}
// Hook for update awareness
import { useUpdates } from "expo-updates";
function App() {
const { isUpdateAvailable, isUpdatePending } = useUpdates();
useEffect(() => {
if (isUpdatePending) {
// An update has been downloaded and is ready
Updates.reloadAsync();
}
}, [isUpdatePending]);
}
Best Practices
- Use the
fingerprintruntime version policy in Expo managed projects. It automatically detects native changes and prevents incompatible OTA updates. - Always test OTA updates on the
previewchannel before pushing toproduction. - Use
eas secretfor API keys and credentials instead of.envfiles in CI. Secrets are injected at build time and never appear in logs. - Set
"autoIncrement": truein eas.json and use"appVersionSource": "remote"so EAS manages build numbers. This avoids conflicts in team environments. - Implement staged rollouts for production OTA updates, starting at 10-25% and monitoring crash rates before full rollout.
- Always include a meaningful
--messagewith OTA updates for auditability.
Common Pitfalls
- OTA updates that require native changes: If you add a new native module and push an OTA update, the app will crash because the native code is not present. The fingerprint runtime policy prevents this, but custom policies require vigilance.
- Forgetting to bump runtime version: When using
appVersionpolicy, failing to bump the version after native changes causes OTA updates to target incompatible builds. - Large OTA bundles: OTA updates download the entire JS bundle. Large bundles on slow connections lead to timeouts. Monitor bundle size with
npx expo export --dump-sourcemap. - Not testing the update flow: Test
Updates.checkForUpdateAsync()andreloadAsync()in preview builds. The update flow is invisible in development mode. - Store review rejections: Apple may reject apps that change core functionality via OTA updates. OTA should be used for bug fixes and minor improvements, not feature overhauls that change the app's purpose.
- Missing
--non-interactivein CI: EAS commands prompt for input by default. Always pass--non-interactivein CI/CD pipelines, or builds will hang.
Install this skill directly: skilldb add react-native-skills
Related Skills
Reanimated Animations
High-performance animations in React Native using Reanimated and Gesture Handler
Expo Managed Workflow
Expo managed workflow for rapid React Native development with minimal native configuration
Native Modules Turbo Modules
Creating native modules and Turbo Modules to bridge platform-specific functionality into React Native
React Navigation Patterns
React Navigation patterns for stack, tab, drawer, and nested navigators in React Native
Offline Storage
Offline storage strategies in React Native using AsyncStorage, MMKV, and WatermelonDB
State Management Zustand Jotai
State management in React Native using Zustand and Jotai for scalable, performant app state