Back to Blog

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.

Viprasol Tech Team
October 6, 2026
13 min read

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

ApproachBuild TimeOTA UpdatesMonthly Cost
Expo Go (development)Instant (no build)N/AFree
EAS Build free tier15โ€“30 min1,000 updatesFree
EAS Build production tier15โ€“30 minUnlimited$99/month
EAS EnterprisePriority queueUnlimitedCustom
Self-hosted (Turtle CLI)20โ€“40 minN/A~$50/month infra

See Also


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.

Mobile app development โ†’ | Start a project โ†’

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol ยท Web Development

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.