React Native with Expo EAS: OTA Updates, Build Profiles, and CI/CD
Master Expo Application Services (EAS): configure build profiles for development, preview, and production, deploy over-the-air updates with EAS Update, set up CI/CD with GitHub Actions, and manage environment variables securely.
Expo Application Services (EAS) is the production deployment platform for React Native apps. It solves the two hardest problems in mobile development: the slow native build cycle (EAS Build) and the week-long App Store review cycle for every bug fix (EAS Update).
EAS Update โ Expo's over-the-air update system โ lets you push JavaScript and asset changes to users instantly, bypassing App Store review for anything that doesn't touch native code.
Project Setup
# Initialize EAS in an existing Expo project
npx expo install expo-updates
eas init # Creates a project on expo.dev and writes project ID to app.json
# Install EAS CLI globally
npm install -g eas-cli
eas login
// app.json
{
"expo": {
"name": "MyApp",
"slug": "myapp",
"version": "1.0.0",
"owner": "viprasol",
"runtimeVersion": {
"policy": "appVersion" // OTA updates target matching app version
},
"updates": {
"url": "https://u.expo.dev/[your-project-id]",
"enabled": true,
"fallbackToCacheTimeout": 30000
},
"android": {
"package": "com.viprasol.myapp",
"versionCode": 1
},
"ios": {
"bundleIdentifier": "com.viprasol.myapp",
"buildNumber": "1"
},
"extra": {
"eas": {
"projectId": "your-project-id-from-expo-dev"
}
}
}
}
EAS Build Profiles
// eas.json โ the EAS configuration file
{
"cli": {
"version": ">= 12.0.0",
"requireCommit": true // Fail if there are uncommitted changes
},
"build": {
// Development builds: includes Expo dev client, connects to Metro
"development": {
"developmentClient": true,
"distribution": "internal",
"channel": "development",
"env": {
"APP_ENV": "development"
},
"android": {
"buildType": "apk" // APK for direct install (faster than AAB)
},
"ios": {
"simulator": false,
"enterpriseProvisioning": "adhoc"
}
},
// Preview builds: production JS, internal distribution (TestFlight/Firebase)
"preview": {
"distribution": "internal",
"channel": "preview",
"env": {
"APP_ENV": "staging"
},
"android": {
"buildType": "apk"
},
"ios": {
"simulator": false
}
},
// Production builds: App Store / Google Play submission
"production": {
"distribution": "store",
"channel": "production",
"env": {
"APP_ENV": "production"
},
"android": {
"buildType": "app-bundle" // AAB required for Play Store
},
"ios": {
"autoIncrement": "buildNumber" // Auto-increment build number
},
"cache": {
"disabled": false,
"key": "production-v1"
}
}
},
"submit": {
"production": {
"android": {
"serviceAccountKeyPath": "./google-play-service-account.json",
"track": "internal", // Start in internal testing, promote manually
"releaseStatus": "draft"
},
"ios": {
"appleId": "your@apple-developer-email.com",
"ascAppId": "1234567890", // App Store Connect app ID
"appleTeamId": "TEAMID"
}
}
}
}
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Environment Variables in EAS
Environment variables in EAS require a specific pattern to be embedded in the JS bundle:
// src/config/env.ts
// Use expo-constants for runtime config, process.env for build-time
import Constants from "expo-constants";
// Values available at runtime (from app.json extra or EAS)
interface AppConfig {
apiUrl: string;
sentryDsn: string;
analyticsKey: string;
environment: "development" | "staging" | "production";
}
function getConfig(): AppConfig {
const env = process.env.APP_ENV as AppConfig["environment"] ?? "development";
return {
// process.env is inlined at build time by Metro bundler
apiUrl: process.env.API_URL ?? "http://localhost:3000",
sentryDsn: process.env.SENTRY_DSN ?? "",
analyticsKey: process.env.ANALYTICS_KEY ?? "",
environment: env,
};
}
export const config = getConfig();
# Store secrets in EAS (encrypted, not in eas.json)
eas secret:create --scope project --name API_URL --value "https://api.viprasol.com"
eas secret:create --scope project --name SENTRY_DSN --value "https://..."
eas secret:create --scope project --name ANALYTICS_KEY --value "..."
# Secrets are automatically available as process.env during builds
# List all secrets (values hidden)
eas secret:list
EAS Update: Over-the-Air Updates
// src/hooks/useOTAUpdate.ts
// Check for and apply updates when app comes to foreground
import { useEffect } from "react";
import * as Updates from "expo-updates";
import { AppState, AppStateStatus, Alert } from "react-native";
export function useOTAUpdate() {
useEffect(() => {
// Don't run in development (Expo Go / dev client handles this differently)
if (__DEV__) return;
const subscription = AppState.addEventListener(
"change",
async (nextAppState: AppStateStatus) => {
if (nextAppState !== "active") return;
try {
const update = await Updates.checkForUpdateAsync();
if (!update.isAvailable) return;
// Download silently in background
await Updates.fetchUpdateAsync();
// For critical updates: reload immediately
// For non-critical: prompt user
Alert.alert(
"Update Available",
"A new version of the app is ready. Restart to apply.",
[
{ text: "Later", style: "cancel" },
{
text: "Restart Now",
onPress: async () => {
await Updates.reloadAsync();
},
},
]
);
} catch (error) {
// Update check failed โ non-fatal, app continues to work
console.warn("OTA update check failed:", error);
}
}
);
return () => subscription.remove();
}, []);
}
Update Channels and Rollout Strategy
# Channels map to build profiles in eas.json
# Publish an update to the preview channel
eas update --channel preview --message "Fix crash on settings screen"
# Publish to production with rollout percentage
eas update --channel production --message "v1.2.1 - Performance improvements" \
--rollout-percentage 10 # Start with 10% of users
# Monitor the rollout, then expand
eas update:view # Check error rates on the update
eas update --channel production --message "v1.2.1" --rollout-percentage 100
# Roll back if needed
eas update:republish --channel production --group <previous-update-group-id>
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
CI/CD with GitHub Actions
# .github/workflows/eas-build.yml
name: EAS Build and Update
on:
push:
branches:
- main # Triggers preview build + OTA update
- production # Triggers production build + submit
pull_request:
branches: [main]
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} # Authenticate with expo.dev
jobs:
# Run on every PR: type check + tests
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npx tsc --noEmit
- run: npm test -- --watchAll=false
# Push to main: publish OTA update to preview channel
update-preview:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- name: Install EAS CLI
run: npm install -g eas-cli
- name: Publish OTA update to preview
run: |
eas update \
--channel preview \
--message "Auto-update from commit ${{ github.sha }}" \
--non-interactive
# Push to production branch: build + submit
build-production:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/production' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- name: Install EAS CLI
run: npm install -g eas-cli
- name: Build for production
run: |
eas build \
--platform all \
--profile production \
--non-interactive \
--wait
- name: Submit to stores
run: |
eas submit \
--platform all \
--profile production \
--non-interactive \
--latest # Submit the build we just created
Runtime Version Strategy
The runtime version controls which OTA updates are compatible with which builds:
// Option 1: appVersion policy (recommended for most teams)
// OTA updates only target devices running the matching app version
// Update 1.2.x only reaches users on app version 1.2.x
{
"runtimeVersion": { "policy": "appVersion" }
}
// Option 2: nativeVersion policy
// OTA updates target matching native code hash
// More granular โ update reaches users with identical native code
{
"runtimeVersion": { "policy": "nativeVersion" }
}
// Option 3: Explicit string (full control)
// You manually bump when native code changes
{
"runtimeVersion": "1.2"
}
The rule: bump the runtime version when you change native code (new native module, updated expo version, modified android/ or ios/ directories). OTA updates don't require a runtime version bump.
Cost Comparison
| Approach | Build Time | OTA Updates | Monthly Cost |
|---|---|---|---|
| Expo Go (development) | Instant (no build) | N/A | Free |
| EAS Build free tier | 15โ30 min | 1,000 updates | Free |
| EAS Build production tier | 15โ30 min | Unlimited | $99/month |
| EAS Enterprise | Priority queue | Unlimited | Custom |
| Self-hosted (Turtle CLI) | 20โ40 min | N/A | ~$50/month infra |
See Also
- React Native Performance Optimization โ JS thread, Hermes, Reanimated
- React Native Testing Strategies โ unit, integration, E2E
- React Native Offline-First Architecture โ offline sync
- Mobile Payment Integration โ Stripe, IAP
Working With Viprasol
EAS dramatically reduces the operational overhead of mobile app deployment. Our React Native team sets up EAS Build profiles, configures OTA update channels for staged rollouts, integrates EAS into CI/CD pipelines, and manages App Store/Play Store submissions โ so your team ships features instead of managing mobile build infrastructure.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.