React Native2025-03-13|9 min read

EAS Build and EAS Update: The Complete Guide to Shipping React Native Apps in 2025

Shipping a React Native app used to mean wrestling with Xcode on a MacBook, managing Android keystores in a shared Dropbox folder, and crossing your fingers every time you triggered a release build. EAS — Expo Application Services — changes that calculus entirely. In my team, we moved from a brittle local build setup to EAS Build and EAS Update about eighteen months ago, and we've never looked back. This guide covers everything I've learned: how the tooling works, how to configure it for real projects, and how to integrate it into a proper CI/CD pipeline.

What Is EAS?

EAS is Expo's suite of cloud services for React Native apps. It sits at three layers of the delivery pipeline: EAS Build (compile your app in the cloud), EAS Submit (upload the resulting binary to the App Store or Play Store), and EAS Update (push JavaScript bundle updates to users without a store release). This guide focuses on Build and Update because they address the two most painful parts of mobile delivery: reproducible native builds and the agonising wait for app store review.

EAS works with both the Expo managed workflow and bare React Native projects. You do not need to use the managed workflow to benefit from cloud builds — any React Native project with an app.json or app.config.js can use EAS Build.

EAS Build vs Local Builds

The case for cloud builds is stronger than it might appear on the surface. The obvious benefit is platform coverage: you cannot run npx expo run:ios on a Linux or Windows machine without a remote Mac. But even on macOS, local builds carry hidden costs.

  • Environment drift. "Works on my machine" is a cliche because it's real. A teammate's Xcode version, CocoaPods cache, or Gradle wrapper can produce a build that differs from yours in ways that are nearly impossible to debug retroactively.
  • Local SDK bloat. A full Android SDK installation with multiple NDK versions, emulator images, and build tools consumes 30–50 GB of disk space and requires constant maintenance as the toolchain evolves.
  • Credential chaos. iOS provisioning profiles and distribution certificates expire, rotate, and need to be shared across a team. Managing this manually in a shared password manager is error-prone and insecure.
  • Machine contention. Running an iOS simulator build locally ties up your CPU for several minutes. On a cloud runner, that build happens in parallel without interrupting your development flow.

EAS Build eliminates all of these problems. The build runs on Expo's managed infrastructure — a macOS VM for iOS, a Linux VM for Android — with pinned Xcode and Gradle versions defined in your configuration. Every developer on the team triggers the same environment with the same command.

Setting Up EAS Build

I typically set up EAS Build on the first day of a new project. The process takes about fifteen minutes and the configuration lives in version control alongside the rest of the codebase.

Start by installing the EAS CLI globally:

npm install -g eas-cli

Authenticate with your Expo account:

eas login

Then run the interactive configuration wizard from your project root:

eas build:configure

This command inspects your project, asks a few questions about your setup, and generates an eas.json file at the project root. Here is a real eas.json that I use as a starting template:

{
  "cli": {
    "version": ">= 10.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "simulator": false
      },
      "channel": "development"
    },
    "preview": {
      "distribution": "internal",
      "ios": {
        "buildConfiguration": "Release"
      },
      "android": {
        "buildType": "apk"
      },
      "channel": "staging"
    },
    "production": {
      "ios": {
        "buildConfiguration": "Release"
      },
      "android": {
        "buildType": "aab"
      },
      "channel": "production"
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "you@example.com",
        "ascAppId": "1234567890"
      },
      "android": {
        "serviceAccountKeyPath": "./google-services-key.json",
        "track": "internal"
      }
    }
  }
}

Once the file is in place, trigger a build with:

# iOS
eas build --platform ios --profile production

# Android
eas build --platform android --profile production

# Both platforms simultaneously
eas build --platform all --profile production

EAS prints a URL you can share with teammates to monitor the build's progress. The resulting artifact — an IPA for iOS or an AAB/APK for Android — is stored on Expo's servers and can be downloaded, shared, or submitted directly from the dashboard.

Build Profiles

Build profiles are the core of eas.json. Each profile is a named configuration that controls how the binary is compiled, signed, and distributed. The three-profile pattern I use maps directly to a real delivery workflow.

Development Profile

The development profile builds a dev client — a custom Expo Go-style app that includes your project's native modules. This is what your engineers install on their physical devices to work against the Metro bundler during development. Setting developmentClient: true and distribution: "internal" tells EAS to sign the app for internal distribution rather than the App Store, which skips the submission review process entirely.

Preview Profile

The preview profile produces a release build for internal testing — stakeholders, QA engineers, and product managers. On iOS it uses distribution: "internal" with ad-hoc provisioning, which means the device UDID must be registered in your Apple Developer account. On Android, building as an APK (rather than an AAB) makes it easy to sideload directly without going through the Play Store.

Production Profile

The production profile builds for store submission. On Android this means an AAB, which the Play Store requires for all new apps. On iOS it uses App Store distribution signing. The channel: "production" setting ties this build to the production OTA update channel — more on that shortly.

Code Signing Made Easy

Code signing is the most error-prone step in traditional mobile CI/CD. EAS abstracts it almost entirely. The first time you run eas build --platform ios, EAS asks whether you want to manage credentials yourself or let EAS handle them. In nearly every project I work on, I choose managed credentials.

With managed credentials, EAS generates and stores your iOS distribution certificate and provisioning profile in Expo's encrypted credential storage. For Android, it generates and stores your upload keystore. You never need to manually export a P12 file, share a keystore password in Slack, or worry about a certificate expiring silently.

To inspect or update your credentials at any time:

eas credentials

This opens an interactive menu that lets you view, rotate, or download your certificates and keystores. If you have an existing keystore from a legacy build pipeline, you can import it through the same interface. The biggest win for me has been eliminating the "where is the keystore?" conversation entirely — it lives in EAS, access-controlled by your Expo organisation membership.

EAS Update for OTA

EAS Update solves a specific and painful problem: once a native binary is in users' hands, fixing a JavaScript bug requires a full app store submission, a review period that ranges from hours to days, and a gradual rollout before most users see the fix. EAS Update bypasses that entirely for JavaScript-only changes.

Under the hood, EAS Update uses the expo-updates library. When your app launches, expo-updates checks a manifest endpoint hosted by Expo's CDN. If a newer update is available on the channel associated with the current build, the library downloads the new JavaScript bundle and assets in the background. On the next app launch, the new bundle runs.

Install the library and initialise the configuration:

npx expo install expo-updates
eas update:configure

Push an update to your users:

eas update --branch production --message "Fix checkout total calculation"

There is one critical limitation to understand: EAS Update can only deliver changes to JavaScript and assets (images, fonts, JSON files). It cannot update native code. If your fix requires a new native module, a change to Info.plist, a new Android permission, or a CocoaPods dependency, you need a full store release. The rule I follow: if the change would require rebuilding the native binary locally, it requires a full build through EAS and a store submission.

Update Channels and Branches

EAS Update uses a channel-to-branch model that maps cleanly onto a standard Git branching strategy. A channel is a named endpoint associated with a build. A branch is a named stream of updates. When you run eas update, you push to a branch; the channel determines which builds receive updates from that branch.

In my typical setup, the production channel in eas.json points to the production branch, and the preview profile points to the staging branch. This means:

  • Running eas update --branch staging --message "Test new onboarding" delivers the update to all devices running a preview build — without touching production users.
  • Running eas update --branch production --message "Fix cart bug" delivers to all production devices immediately.

To reassign a channel to a different branch — useful for a hotfix or a staged rollout:

eas channel:edit production --branch hotfix-payment

This is one of the most powerful operational levers in the EAS toolkit. In an incident, you can redirect the entire production channel to a known-good branch in seconds, without waiting for a store review.

Rollbacks and Monitoring

EAS Update tracks every update you publish. If a bad update goes out — a crash regression, a broken API integration, or a UI that doesn't render correctly on a specific device — you can roll back immediately:

eas update:rollback --branch production

This command republishes the previous update on the branch. Devices running expo-updates will receive the rollback on their next check-in, which is typically the next app launch.

For monitoring, EAS Insights (available on paid plans) provides update adoption metrics: what percentage of your active user base has received a given update, broken down by platform and app version. This is critical for understanding exposure during an incident. If you pushed a bad update four hours ago and Insights shows 62% adoption, you know the scope of the problem before you begin investigating.

I also recommend coupling EAS Update with crash reporting (Sentry or Bugsnag). Configure your crash reporter to tag events with the EAS update ID — this lets you correlate a spike in crashes directly to a specific update publication.

CI/CD Integration

EAS Build and EAS Update work well in isolation, but the real productivity gain comes from automating them in a CI/CD pipeline. Here is the GitHub Actions setup I use across most of my projects.

First, store your Expo token as a GitHub Actions secret named EXPO_TOKEN. Generate it from your Expo account settings.

Then create .github/workflows/eas.yml:

name: EAS Build and Update

on:
  push:
    branches:
      - main
    tags:
      - 'v*'

jobs:
  update:
    name: EAS Update (main branch)
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Setup EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

      - name: Publish OTA update
        run: eas update --branch production --message "Deploy ${{ github.sha }}" --non-interactive

  build:
    name: EAS Build (release tag)
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Setup EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

      - name: Build iOS and Android
        run: eas build --platform all --profile production --non-interactive

The --non-interactive flag is essential in CI — without it, EAS may prompt for input that the runner cannot provide. The expo-github-action step handles installing the EAS CLI and authenticating with your token automatically, so no manual CLI setup is required on the runner.

The workflow triggers an OTA update on every push to main (fast, no store review required), and triggers a full native build whenever you push a version tag like v1.4.0 (for store submission).

Pricing and Limits

EAS Build's free tier includes 30 iOS builds and 30 Android builds per month. For small teams and solo developers, this is sufficient for a steady cadence of releases without spending anything. Builds run on shared infrastructure, which means queue times can stretch to fifteen or twenty minutes during peak hours.

Paid plans (starting at $99/month for teams) unlock priority build queues, which typically deliver builds in under five minutes, as well as higher monthly build limits and concurrent builds. For a team shipping weekly releases across two platforms, the time saved on queue waits alone justifies the cost.

EAS Update charges based on monthly active users (MAUs) receiving updates. The free tier covers up to 1,000 MAUs, which is generous for early-stage apps. Bandwidth costs kick in above that threshold at a rate that is competitive with running your own CDN-backed update server — and you get the managed infrastructure and Insights dashboard without any operational overhead.

My recommendation: start on the free tier, validate the workflow, and upgrade to a paid build plan when queue times start to affect your team's deployment cadence. The transition is seamless — there are no configuration changes required when you upgrade, only faster queues and higher limits.