From Build to App Store: Deploying React Native Apps with EAS in 2026

A complete, practical guide to deploying React Native apps with Expo Application Services (EAS) in 2026 — from build profiles and credential management to OTA updates and CI/CD automation.

Shipping a React Native app used to be one of the most dreaded parts of mobile development. Between wrestling with Xcode signing certificates, deciphering cryptic Android keystore errors, and manually uploading binaries to two different app stores, it was easy to spend more time on deployment than on actually building features. I've been there — staring at a "code signing" error at 11 PM, wondering why I ever left web development.

In 2026, that reality has fundamentally changed.

Expo Application Services (EAS) has evolved into a mature, production-grade deployment pipeline that handles everything from building your binaries in the cloud to submitting them to app stores and pushing over-the-air updates to your users — all from the command line. Whether you're a solo developer shipping your first app or a team managing dozens of releases per month, this guide walks you through every step of deploying React Native apps with EAS, complete with practical code examples you can adapt to your own projects.

Why Deployment Matters More Than Ever

The mobile ecosystem in 2026 demands faster release cycles, tighter security, and seamless user experiences. Users expect bug fixes within hours, not weeks. App store review processes have gotten more stringent, and both Apple and Google now enforce stricter requirements around signing, privacy manifests, and SDK declarations.

A manual deployment process isn't just slow — it's a liability.

EAS addresses these challenges head-on. With Expo SDK 53+ and React Native 0.79+, EAS has matured into a deployment platform that handles cloud builds, credential management, app store submission, over-the-air updates, and CI/CD workflows in a single unified system. It works with any React Native project, whether you started with Expo or added it later through the expo prebuild flow. The tooling is stable, the documentation is comprehensive, and honestly, the developer experience is the best it's ever been.

Understanding the EAS Ecosystem

Before diving into configuration, it's worth understanding the four pillars of EAS and how they work together as an integrated deployment pipeline.

EAS Build

EAS Build compiles your React Native project in the cloud, producing installable binaries — .ipa files for iOS and .aab or .apk files for Android. You don't need a Mac to build for iOS (yes, really). EAS manages the build servers, Xcode and Android SDK versions, and native dependencies. You define build profiles in eas.json, and EAS handles the rest.

EAS Submit

EAS Submit automates the upload of your built binaries to the Apple App Store (via App Store Connect) and the Google Play Store (via the Google Play Console). It handles authentication, metadata validation, and upload retries. You can trigger submission manually or chain it automatically after a successful build.

EAS Update

EAS Update enables over-the-air (OTA) updates for your JavaScript bundle and assets without requiring a full app store submission. Bug fixes, copy changes, and minor feature updates can reach users in minutes rather than days. Updates are delivered through a channel and branch system that gives you fine-grained control over which users receive which updates.

EAS Workflows

EAS Workflows is the CI/CD layer that ties everything together. Using YAML configuration files, you can define automated pipelines that trigger builds, run tests, publish updates, and submit to app stores based on events like pull requests or merges to specific branches. It integrates natively with GitHub and supports custom logic at every step.

Together, these four services create a pipeline where code goes from your editor to your users' devices with minimal manual intervention. So, let's set it up.

Setting Up Your First EAS Project

Getting started with EAS requires a few prerequisites: an Expo account, Node.js 18 or later, and the EAS CLI. If you already have a React Native project, you don't need to eject or restructure anything — EAS works with your existing project structure.

Installing the EAS CLI

# Install the EAS CLI globally
npm install -g eas-cli

# Verify the installation
eas --version

# Log in to your Expo account
eas login

Initializing EAS in Your Project

Navigate to your React Native project root and run the configuration command:

# Configure EAS for your project
eas build:configure

This command does three things. First, it links your local project to an Expo project on the EAS servers (creating one if needed). Second, it generates an eas.json file in your project root. Third, it ensures your app.json or app.config.js has the required fields like ios.bundleIdentifier and android.package.

Understanding the Generated eas.json

The generated eas.json file is the heart of your EAS configuration. Here's a well-structured starting point that covers the most common deployment scenarios:

{
  "cli": {
    "version": ">= 14.2.0",
    "appVersionSource": "remote"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "simulator": false
      },
      "env": {
        "APP_ENV": "development"
      }
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview",
      "env": {
        "APP_ENV": "staging"
      }
    },
    "production": {
      "channel": "production",
      "autoIncrement": true,
      "env": {
        "APP_ENV": "production"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "ascAppId": "YOUR_APP_STORE_CONNECT_APP_ID",
        "appleTeamId": "YOUR_APPLE_TEAM_ID"
      },
      "android": {
        "serviceAccountKeyPath": "./google-service-account.json",
        "track": "internal"
      }
    }
  }
}

The cli section specifies the minimum EAS CLI version and tells EAS to manage app version numbers remotely, which prevents version conflicts when multiple team members are building. The build section defines profiles for different stages of development. The submit section configures how binaries are delivered to the app stores. Let's explore each of these in more detail.

Configuring Build Profiles in eas.json

Build profiles are where the real power of EAS configuration lives. Each profile defines a complete set of build parameters — distribution type, signing configuration, environment variables, native build settings, and more. You can have as many profiles as you need, and they can extend each other to reduce duplication.

Development Builds with expo-dev-client

Development builds include the expo-dev-client package, which replaces Expo Go with a custom development client that supports your specific native modules. These builds get installed directly on developer devices or simulators and connect to your local Metro bundler.

{
  "development": {
    "developmentClient": true,
    "distribution": "internal",
    "ios": {
      "simulator": false,
      "resourceClass": "m-medium"
    },
    "android": {
      "buildType": "apk",
      "resourceClass": "medium"
    },
    "env": {
      "APP_ENV": "development",
      "API_URL": "http://localhost:3000"
    }
  },
  "development:simulator": {
    "extends": "development",
    "ios": {
      "simulator": true
    }
  }
}

Note the development:simulator profile that extends the base development profile. This creates an iOS simulator build while inheriting all other settings. The colon syntax is just a naming convention — you can name profiles anything you want.

Preview and Staging Builds

Preview builds are production-like binaries distributed internally to your team for testing. They don't include the development client and instead bundle your JavaScript code, just like a production build. The difference? They use internal distribution, meaning they're installed via a link or QR code rather than through the app store.

{
  "preview": {
    "distribution": "internal",
    "channel": "preview",
    "ios": {
      "resourceClass": "m-medium"
    },
    "android": {
      "buildType": "apk"
    },
    "env": {
      "APP_ENV": "staging",
      "API_URL": "https://api-staging.yourapp.com"
    }
  }
}

Setting "buildType": "apk" for Android preview builds makes them easier to distribute — APK files can be installed directly, whereas AAB files (the default) require Google Play to process them. For iOS, internal distribution uses ad-hoc provisioning profiles, and EAS automatically manages device registration through its device management portal.

Production Builds for App Store Submission

Production profiles generate optimized binaries ready for app store submission. For iOS, that means a signed .ipa with a distribution provisioning profile. For Android, a signed .aab (Android App Bundle) ready for Google Play.

{
  "production": {
    "distribution": "store",
    "channel": "production",
    "autoIncrement": true,
    "ios": {
      "resourceClass": "m-large"
    },
    "android": {
      "buildType": "app-bundle",
      "resourceClass": "large"
    },
    "env": {
      "APP_ENV": "production",
      "API_URL": "https://api.yourapp.com"
    }
  }
}

The autoIncrement field automatically bumps the build number (iOS buildNumber / Android versionCode) on each build, eliminating a common source of submission rejections. The resourceClass fields allocate more powerful build machines for production builds, which can cut build times significantly.

Custom Profiles for QA and Beta Testing

Real-world projects often need additional profiles beyond the basic three. Here's how you might configure profiles for QA testing and public beta programs:

{
  "qa": {
    "extends": "preview",
    "channel": "qa",
    "env": {
      "APP_ENV": "qa",
      "API_URL": "https://api-qa.yourapp.com",
      "ENABLE_DEBUG_MENU": "true",
      "LOG_LEVEL": "verbose"
    }
  },
  "beta": {
    "extends": "production",
    "channel": "beta",
    "distribution": "store",
    "autoIncrement": true,
    "android": {
      "track": "beta"
    },
    "env": {
      "APP_ENV": "beta",
      "API_URL": "https://api.yourapp.com",
      "FEATURE_FLAGS_OVERRIDE": "new_onboarding,redesigned_profile"
    }
  }
}

The extends keyword is essential for keeping your configuration DRY. The qa profile inherits everything from preview and only overrides the channel and environment variables. The beta profile extends production but routes to the beta track on Google Play and enables specific feature flags.

A Complete, Production-Ready eas.json

Here's a comprehensive eas.json that brings all of these profiles together:

{
  "cli": {
    "version": ">= 14.2.0",
    "appVersionSource": "remote",
    "promptToConfigurePushNotifications": true
  },
  "build": {
    "base": {
      "node": "20.11.0",
      "ios": {
        "resourceClass": "m-medium"
      },
      "android": {
        "resourceClass": "medium"
      }
    },
    "development": {
      "extends": "base",
      "developmentClient": true,
      "distribution": "internal",
      "android": {
        "buildType": "apk"
      },
      "env": {
        "APP_ENV": "development"
      }
    },
    "development:simulator": {
      "extends": "development",
      "ios": {
        "simulator": true
      }
    },
    "preview": {
      "extends": "base",
      "distribution": "internal",
      "channel": "preview",
      "android": {
        "buildType": "apk"
      },
      "env": {
        "APP_ENV": "staging"
      }
    },
    "qa": {
      "extends": "preview",
      "channel": "qa",
      "env": {
        "APP_ENV": "qa",
        "ENABLE_DEBUG_MENU": "true"
      }
    },
    "production": {
      "extends": "base",
      "distribution": "store",
      "channel": "production",
      "autoIncrement": true,
      "ios": {
        "resourceClass": "m-large"
      },
      "android": {
        "buildType": "app-bundle",
        "resourceClass": "large"
      },
      "env": {
        "APP_ENV": "production"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "ascAppId": "6449012345",
        "appleTeamId": "ABCDE12345"
      },
      "android": {
        "serviceAccountKeyPath": "./play-store-credentials.json",
        "track": "production",
        "releaseStatus": "draft"
      }
    }
  }
}

Notice the base profile at the top. While it's never used directly for a build, it serves as a common ancestor that locks down the Node.js version and default resource classes. Every other profile extends from it directly or indirectly, ensuring consistency across all build types.

Managing Credentials and App Signing

Code signing is one of the most confusing aspects of mobile development, and it's also one of the areas where EAS provides the most value. EAS offers two approaches to credential management: auto-managed and local credentials.

Auto-Managed Credentials (Recommended)

By default, EAS manages all signing credentials for you. When you run your first build, EAS generates, stores, and manages all the certificates and keys needed to sign your app. For iOS, this includes distribution certificates, provisioning profiles, and push notification keys. For Android, the upload keystore and signing key.

# Run a build and let EAS handle credentials automatically
eas build -p ios --profile production

# EAS will prompt you:
# ? Would you like EAS to manage your iOS credentials? (Y/n) Y
# Generated new Distribution Certificate
# Generated new Provisioning Profile
# Build started successfully

Auto-managed credentials are stored securely on EAS servers with encryption at rest. This is the recommended approach for most teams because it eliminates the risk of losing certificates, simplifies onboarding for new team members, and handles certificate renewal automatically. Honestly, unless you have a specific compliance reason not to, just use this.

Local Credentials

Some organizations require credentials to be managed internally due to compliance or security policies. EAS supports this through the credentialsSource field:

{
  "production": {
    "ios": {
      "credentialsSource": "local"
    },
    "android": {
      "credentialsSource": "local"
    }
  }
}

When using local credentials, you provide the signing artifacts in a credentials.json file at your project root:

{
  "ios": {
    "distributionCertificate": {
      "path": "./certs/distribution.p12",
      "password": "CERT_PASSWORD"
    },
    "provisioningProfilePath": "./certs/profile.mobileprovision"
  },
  "android": {
    "keystore": {
      "keystorePath": "./certs/release.keystore",
      "keystorePassword": "KEYSTORE_PASSWORD",
      "keyAlias": "release-key",
      "keyPassword": "KEY_PASSWORD"
    }
  }
}

Important: Never commit credentials.json or your actual certificate files to version control. Add them to your .gitignore immediately and use secure secrets management in your CI/CD pipeline to provide them at build time. I've seen teams learn this one the hard way.

Using eas credentials for Management

The eas credentials command provides an interactive interface for managing your signing credentials:

# Launch the interactive credential manager
eas credentials

# Or target a specific platform
eas credentials -p ios

# Common operations available:
# - View current credentials
# - Download certificates and profiles
# - Upload existing certificates
# - Remove and regenerate credentials
# - Set up push notification keys

Security Best Practices

  • Use auto-managed credentials unless your organization requires otherwise. EAS encrypts credentials at rest and in transit.
  • Enable two-factor authentication on your Expo account and Apple Developer account.
  • Rotate credentials periodically, especially if a team member with access leaves the organization.
  • Use service accounts for CI/CD rather than personal accounts. Both Apple and Google support service-level authentication.
  • Audit credential access regularly through the EAS dashboard to see which team members have accessed signing certificates.

Environment Variables and Secrets

Every non-trivial app needs different configuration for different environments — API endpoints, analytics keys, feature flags, and more. EAS provides a layered system for managing environment variables that balances convenience with security.

Setting Environment Variables in eas.json

The simplest approach is to define environment variables directly in your build profiles, as shown in earlier examples. These variables are available during the build process and can be accessed in your application code:

// app.config.js - access env vars at build time
export default {
  name: process.env.APP_ENV === 'production' ? 'MyApp' : `MyApp (${process.env.APP_ENV})`,
  slug: 'my-app',
  version: '2.1.0',
  extra: {
    apiUrl: process.env.API_URL,
    environment: process.env.APP_ENV,
  },
};

// In your application code, access via expo-constants
import Constants from 'expo-constants';

const API_URL = Constants.expoConfig?.extra?.apiUrl;
const ENVIRONMENT = Constants.expoConfig?.extra?.environment;

Using the EAS Dashboard for Secrets

Sensitive values like API keys, authentication tokens, and third-party service credentials should never appear in eas.json or any committed file. Instead, use the EAS dashboard or CLI to set secrets:

# Set a secret via the CLI
eas secret:create --name SENTRY_AUTH_TOKEN --value "sntrys_your_token_here" --scope project

# Set a secret with specific visibility
eas secret:create --name ANALYTICS_KEY --value "ak_live_xxxxx" --scope project --type secret

# List all secrets for the project
eas secret:list

# Delete a secret
eas secret:delete --name OLD_API_KEY

Visibility Levels

EAS supports three visibility levels for environment variables and secrets:

  • Plain text — Visible in eas.json, build logs, and the dashboard. Use for non-sensitive configuration like environment names and feature flags.
  • Sensitive — Stored on EAS servers and injected at build time, but masked in build logs. Use for values that aren't critically secret but shouldn't be casually visible, like analytics keys.
  • Secret — Encrypted at rest, never visible in logs or the dashboard after creation, and only available during the build process. Use for authentication tokens, private API keys, and signing-related credentials.

Common Patterns for Multi-Environment Configuration

Here's a pattern I've found works really well for managing configuration across multiple environments:

// config/environment.js
const ENVIRONMENTS = {
  development: {
    apiUrl: 'http://localhost:3000',
    sentryDsn: null,
    enableAnalytics: false,
    logLevel: 'debug',
  },
  staging: {
    apiUrl: 'https://api-staging.yourapp.com',
    sentryDsn: process.env.SENTRY_DSN,
    enableAnalytics: false,
    logLevel: 'info',
  },
  production: {
    apiUrl: 'https://api.yourapp.com',
    sentryDsn: process.env.SENTRY_DSN,
    enableAnalytics: true,
    logLevel: 'error',
  },
};

const currentEnv = process.env.APP_ENV || 'development';
export default ENVIRONMENTS[currentEnv];

This approach keeps your environment-specific configuration organized and type-safe while relying on EAS to provide the correct APP_ENV value for each build profile.

Building and Submitting to App Stores

With your profiles configured and credentials in place, building and submitting is surprisingly straightforward. Let's walk through the complete process for both platforms.

Triggering Production Builds

# Build for iOS
eas build -p ios --profile production

# Build for Android
eas build -p android --profile production

# Build for both platforms simultaneously
eas build --profile production

# Build with a custom message for tracking
eas build --profile production --message "Release 2.1.0 - new onboarding flow"

Each build command uploads your project to EAS servers, where it's compiled in a clean environment. You can monitor build progress in your terminal or on the EAS dashboard. Build times vary depending on your project size and the resource class you selected — typically 10 to 20 minutes for iOS and 8 to 15 minutes for Android.

Configuring Submit Profiles

Before your first submission, you'll need to set up authentication with both app stores. For Apple, you'll need an App Store Connect API key. For Google, a service account with the appropriate Play Console permissions.

{
  "submit": {
    "production": {
      "ios": {
        "ascAppId": "6449012345",
        "appleTeamId": "ABCDE12345",
        "ascApiKeyPath": "./AuthKey_XXXXXXXXXX.p8",
        "ascApiKeyIssuerId": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "ascApiKeyId": "XXXXXXXXXX"
      },
      "android": {
        "serviceAccountKeyPath": "./play-store-credentials.json",
        "track": "production",
        "releaseStatus": "draft",
        "changesNotSentForReview": false
      }
    },
    "beta": {
      "ios": {
        "ascAppId": "6449012345",
        "appleTeamId": "ABCDE12345"
      },
      "android": {
        "serviceAccountKeyPath": "./play-store-credentials.json",
        "track": "beta",
        "releaseStatus": "completed"
      }
    }
  }
}

Submitting to the App Stores

# Submit the latest iOS build to App Store Connect
eas submit -p ios --profile production --latest

# Submit the latest Android build to Google Play
eas submit -p android --profile production --latest

# Submit a specific build by ID
eas submit -p ios --profile production --id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Auto-Submit After Build

For a fully automated pipeline, you can chain build and submit into a single command:

# Build and automatically submit when the build completes
eas build -p ios --profile production --auto-submit
eas build -p android --profile production --auto-submit

# Build both platforms and auto-submit both
eas build --profile production --auto-submit

The --auto-submit flag tells EAS to automatically submit the binary to the appropriate app store once the build succeeds. This is extremely useful in CI/CD pipelines where you want a completely hands-off release process.

App Store Setup Tips

  • Apple App Store Connect: Create an API key under Users and Access > Integrations > App Store Connect API. Give it the "App Manager" role for submission access. Store the .p8 key file securely and reference it in your submit profile or upload it as an EAS secret.
  • Google Play Console: Create a service account in Google Cloud Console, grant it "Release Manager" access in Play Console under Settings > API access, and download the JSON key file. One gotcha here: the first upload to a new track must always be done manually through the Play Console.
  • Version management: Set "appVersionSource": "remote" in the cli section of eas.json and use "autoIncrement": true in production profiles to let EAS manage build numbers automatically.

Over-the-Air Updates with EAS Update

This is, in my opinion, one of the most powerful features of the entire EAS ecosystem. EAS Update lets you push updates to your app without going through a full app store review cycle. New JavaScript bundles and assets get downloaded by the app at runtime, providing near-instant delivery of bug fixes and improvements.

How OTA Updates Work

A React Native app consists of two layers: the native runtime (compiled Objective-C/Swift and Java/Kotlin code) and the JavaScript bundle (your React components, business logic, and assets). OTA updates replace only the JavaScript bundle layer.

This means you can update anything written in JavaScript — screens, components, styles, navigation, API calls, images — but you can't change native modules, native configuration (like permissions in Info.plist), or the app's native dependencies.

When your app launches, the expo-updates library checks for new updates on the EAS servers. If a compatible update is found, it's downloaded in the background and applied on the next app launch. The whole process is transparent to the user and happens in milliseconds.

Setting Up Runtime Version Policies

Runtime versioning is the mechanism that ensures OTA updates are only applied to compatible native builds. If you add a new native module and publish an OTA update, devices running the old native build must not receive that update. EAS supports several runtime version policies:

{
  "expo": {
    "runtimeVersion": {
      "policy": "fingerprint"
    }
  }
}

The fingerprint policy (recommended for most projects) automatically generates a hash based on your native project configuration, installed native modules, and Expo SDK version. If anything in the native layer changes, the fingerprint changes, and a new build is required. This is the safest approach because it prevents incompatible updates from ever reaching devices.

Alternatively, you can use the appVersion policy, which ties the runtime version to your app version string, or set a custom runtime version string manually:

{
  "expo": {
    "runtimeVersion": "1.0.0"
  }
}

Channels and Branches

EAS Update uses a two-level routing system: channels and branches. A channel is associated with a build profile and determines which updates a specific build will receive. A branch is a stream of updates, similar to a Git branch. You map channels to branches to control update delivery.

# Publish an update to the production branch
eas update --branch production --message "Fix: resolve crash on checkout screen"

# Publish an update to the preview branch
eas update --branch preview --message "Feature: add new payment method UI"

# View the current channel-to-branch mappings
eas channel:list

# Point the production channel to a specific branch
eas channel:edit production --branch production

# Create a rollback by pointing production to a previous branch
eas channel:edit production --branch production-rollback

This channel and branch system enables some powerful deployment strategies. For example, you can create a hotfix branch for emergency fixes, test it by pointing the preview channel to it, and then atomically promote it to production by updating the channel mapping. Pretty elegant, right?

Publishing Updates

# Publish an update with a descriptive message
eas update --branch production --message "v2.1.1: Fix payment processing timeout"

# Publish with a specific platform
eas update --branch production --platform ios --message "iOS-specific keyboard fix"

# Publish and include all assets
eas update --branch production --message "Update images and translations"

Update Rollback Strategies

If an OTA update introduces a regression, you need to roll back quickly. EAS provides several strategies for this:

# Strategy 1: Publish a new update that reverts the changes
# (simply revert your code and publish again)
git revert HEAD
eas update --branch production --message "Rollback: revert payment changes"

# Strategy 2: Re-point the channel to a known-good branch
eas channel:edit production --branch production-v2.1.0-stable

# Strategy 3: Republish a specific previous update group
eas update:republish --group xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --branch production

When to Use OTA vs Full Build

Understanding when you can use an OTA update versus when you need a full build is critical. Getting this wrong can lead to crashes for your users, so pay attention here:

  • Use OTA updates for: Bug fixes in JavaScript code, UI changes, copy and translation updates, asset changes (images, fonts), configuration changes in JavaScript, adding or modifying API calls, navigation changes.
  • Require a full build for: Adding or updating native modules, changing native configuration (permissions, deep links), updating the Expo SDK version, modifying the app.json fields that affect the native project, adding new native dependencies to package.json.

When in doubt, use the fingerprint runtime version policy. If the fingerprint has changed since your last build, you need a full build. If it hasn't, an OTA update will work just fine.

CI/CD with EAS Workflows and GitHub Actions

Automating your deployment pipeline is the final step in building a truly production-grade release process. EAS supports automation through its native Workflows feature and through integration with third-party CI/CD systems like GitHub Actions.

EAS Workflows

EAS Workflows let you define automated pipelines using YAML files in your repository. Create a .eas/workflows directory and add your workflow definitions:

# .eas/workflows/production-release.yml
name: Production Release
on:
  push:
    branches: ['main']

jobs:
  build_ios:
    name: Build iOS
    type: build
    platform: ios
    profile: production
    params:
      auto_submit: true

  build_android:
    name: Build Android
    type: build
    platform: android
    profile: production
    params:
      auto_submit: true

  notify:
    name: Notify Team
    needs: [build_ios, build_android]
    type: run
    steps:
      - run: |
          curl -X POST "$SLACK_WEBHOOK_URL" \
            -H 'Content-Type: application/json' \
            -d '{"text": "Production builds submitted to app stores!"}'
# .eas/workflows/preview-update.yml
name: Preview Update on PR
on:
  pull_request:
    branches: ['main']
    types: [opened, synchronize]

jobs:
  update:
    name: Publish Preview Update
    type: update
    params:
      branch: pr-${{ github.event.pull_request.number }}
      message: "PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}"

GitHub Actions Integration

If you prefer GitHub Actions or need to integrate EAS into an existing CI/CD pipeline, the official expo-github-action makes it straightforward:

# .github/workflows/eas-build.yml
name: EAS Build and Submit
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  preview-update:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    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 Expo
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

      - name: Publish preview update
        run: |
          eas update --branch pr-${{ github.event.pull_request.number }} \
            --message "PR #${{ github.event.pull_request.number }}"

  production-build:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    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 Expo
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

      - name: Build and submit iOS
        run: eas build -p ios --profile production --auto-submit --non-interactive

      - name: Build and submit Android
        run: eas build -p android --profile production --auto-submit --non-interactive

The --non-interactive flag is essential in CI/CD environments. It tells the EAS CLI to never prompt for user input and to fail immediately if any required information is missing. Make sure you set EXPO_TOKEN as a repository secret — you can generate one from the Expo dashboard under Account Settings > Access Tokens.

PR Preview Updates with QR Codes

One of the most impactful automations you can set up is automatic preview updates for pull requests. When a developer opens a PR, the CI pipeline publishes an OTA update to a PR-specific branch. A bot comments on the PR with a QR code that anyone on the team can scan to instantly test the changes on their device. This dramatically speeds up code review for UI changes and pretty much eliminates the "it works on my machine" problem.

The key to making this work is creating a channel mapping for each PR branch:

# In your CI pipeline, after publishing the update:
eas channel:create pr-$PR_NUMBER 2>/dev/null || true
eas channel:edit pr-$PR_NUMBER --branch pr-$PR_NUMBER

Testers using a development or preview build with the preview channel can switch to the PR branch by scanning the QR code, test the changes, and switch back — all without installing a new build.

Production Best Practices and Troubleshooting

Running EAS in production at scale requires attention to several operational concerns. Here are the practices and patterns that experienced teams rely on.

Monitoring Builds and Submissions

EAS provides multiple ways to monitor the status of your builds and submissions:

# List recent builds
eas build:list --limit 10

# View detailed info about a specific build
eas build:view BUILD_ID

# Check the status of submissions
eas submit:list

For team environments, configure webhook notifications to send build status updates to Slack, Microsoft Teams, or your preferred communication tool. You can set this up in the EAS dashboard under Project Settings > Webhooks.

Common Build Failures and Fixes

Even with EAS handling the complexity of native builds, failures do happen. Here are the most common issues and their solutions:

  • "Provisioning profile does not match bundle identifier" — Run eas credentials -p ios and regenerate the provisioning profile. This usually happens after changing the bundle identifier in app.json.
  • "SDK version mismatch" — Ensure your expo package version matches the SDK version specified in app.json. Run npx expo install --fix to align dependency versions.
  • CocoaPods installation failures — Pin a specific build image in your iOS profile to avoid breaking changes from Xcode or CocoaPods updates.
  • "Version code already exists" on Google Play — Enable "autoIncrement": true in your production profile, or manually bump the versionCode in app.json.
  • Out-of-memory errors during Android builds — Increase the resource class to "large" in your Android build profile, or add "GRADLE_OPTS": "-Xmx4g -XX:MaxMetaspaceSize=1g" to your build environment variables.
  • Native module compilation errors — Check that your native dependency versions are compatible with your React Native version. The npx expo-doctor command can identify common incompatibilities.

Version Management Strategy

A disciplined versioning strategy prevents confusion and submission rejections. Here's the recommended approach:

// app.config.js
export default {
  version: '2.1.0', // User-facing version (semver), update manually for releases
  ios: {
    buildNumber: '1', // Managed by EAS autoIncrement
  },
  android: {
    versionCode: 1,   // Managed by EAS autoIncrement
  },
};

Use semantic versioning for your user-facing version string: major versions for breaking changes, minor versions for new features, and patch versions for bug fixes. Let EAS manage the build number and version code automatically with "appVersionSource": "remote" and "autoIncrement": true. This way, you only need to manually update the version string when you want to signal a meaningful change to users.

# View and manage app versions remotely
eas build:version:get -p ios
eas build:version:get -p android

# Manually set a version if needed
eas build:version:set -p ios --build-number 42
eas build:version:set -p android --version-code 42

Cache Management for Faster Builds

EAS caches dependencies and build artifacts between builds to reduce build times. You can configure custom cache keys to optimize this behavior:

{
  "production": {
    "cache": {
      "key": "production-v2",
      "cacheDefaultPaths": true,
      "customPaths": [
        "node_modules/.cache",
        "ios/Pods"
      ]
    }
  }
}

If you encounter stale cache issues (builds succeed locally but fail on EAS, or vice versa), invalidate the cache by changing the cache key:

# Clear the cache for a specific build
eas build -p ios --profile production --clear-cache

Cost Optimization Tips

EAS pricing is based on build minutes and the number of updates. Here are some practical ways to keep costs under control:

  • Use appropriate resource classes. Development and preview builds rarely need the large resource class. Reserve it for production builds where speed matters most.
  • Leverage OTA updates. Every update you can ship via EAS Update instead of a full build saves significant build minutes. For JavaScript-only changes, OTA updates are nearly free compared to full builds.
  • Avoid unnecessary builds. Configure CI to only trigger builds when relevant files change. A documentation change shouldn't trigger a native build.
  • Use the --clear-cache flag sparingly. Cached builds are faster and consume fewer minutes. Only clear the cache when you suspect cache corruption.
  • Consolidate platform builds. Running eas build --profile production without the -p flag builds both platforms simultaneously, which is more efficient than triggering them separately.

Advanced Troubleshooting

For builds that fail in EAS but work locally, these debugging techniques can save you hours:

# Run a local build to compare behavior
eas build --local -p android --profile production

# Check your project configuration for common issues
npx expo-doctor

# Verify that your native project is in sync with your config
npx expo prebuild --clean

The --local flag runs the build on your own machine using the same build steps that EAS would use in the cloud. This is invaluable for debugging environment-specific issues because it eliminates network and server variables from the equation.

Wrapping Up

Deploying React Native apps in 2026 with EAS is a fundamentally different experience than the manual, error-prone process it replaced. By investing time in setting up a proper eas.json configuration with well-defined build profiles, automated credential management, environment-specific variables, and CI/CD workflows, you create a deployment pipeline that genuinely scales with your team and your app.

Here are the key takeaways:

  1. Structure your build profiles thoughtfully. Use a base profile with extends to keep your configuration DRY. Create separate profiles for development, preview, QA, and production environments.
  2. Let EAS manage your credentials. Auto-managed credentials eliminate an entire category of deployment headaches. Only use local credentials if your organization specifically requires it.
  3. Keep secrets out of version control. Use the EAS dashboard or CLI to manage sensitive environment variables, and pick the appropriate visibility level for each value.
  4. Automate everything. Set up CI/CD with EAS Workflows or GitHub Actions to trigger builds on merge and publish preview updates on pull requests. The QR code preview workflow alone will transform your team's code review process.
  5. Use OTA updates aggressively. Every JavaScript-only change that you ship via EAS Update reaches users faster and costs less than a full build cycle.
  6. Monitor and iterate. Watch your build times, track common failure patterns, and refine your configuration over time. The teams that deploy most reliably are the ones that treat their deployment pipeline as a first-class product.

With these patterns in place, you can shift your focus from wrestling with deployment infrastructure to building features that actually delight your users. The path from code to app store has never been shorter or more reliable. Start with the basics, automate incrementally, and before long, deploying to millions of users will feel as natural as pushing a commit.

About the Author Editorial Team

Our team of expert writers and editors.