Back to Blog
Mobile10 min readMarch 5, 2025

Deploying React Native Apps to App Store and Play Store with Expo

The complete guide to deploying React Native apps using Expo EAS Build and Submit. Covers signing certificates, OTA updates, environment configs, and CI/CD automation.

ExpoReact NativeiOSAndroidApp Store
A

Azam

DevOps & AI Consultant

Why EAS Build Changes Everything

Historically, deploying a React Native app required a Mac for iOS builds, Android Studio for Play Store submissions, and careful management of signing certificates that were easy to lose and hard to regenerate. Expo Application Services (EAS) moves all of this to the cloud. You run eas build, Expo's servers build the native binary, and you submit it to the stores — without a Mac, without Android Studio, and with signing credentials managed securely in Expo's infrastructure.

EAS Build Configuration

EAS uses eas.json to define build profiles. You typically need three: development (for device testing with Expo Dev Client), preview (internal distribution), and production (store submission).

{
  "cli": { "version": ">= 10.0.0" },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": { "simulator": false },
      "android": { "buildType": "apk" }
    },
    "preview": {
      "distribution": "internal",
      "ios": { "enterpriseProvisioning": "adhoc" },
      "android": { "buildType": "apk" }
    },
    "production": {
      "autoIncrement": true,
      "ios": { "buildConfiguration": "Release" },
      "android": { "buildType": "aab" }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "your@apple.id",
        "ascAppId": "1234567890"
      },
      "android": {
        "serviceAccountKeyPath": "./google-service-account.json",
        "track": "internal"
      }
    }
  }
}

Environment Configuration for Multiple Environments

Use app.config.ts (dynamic config) instead of app.json to inject environment-specific values. Store secrets in EAS secrets, not in the repo.

// app.config.ts
export default ({ config }) => ({
  ...config,
  name: process.env.APP_ENV === 'production' ? 'MyApp' : 'MyApp (Dev)',
  ios: {
    bundleIdentifier: process.env.APP_ENV === 'production'
      ? 'com.company.myapp'
      : 'com.company.myapp.dev',
  },
  android: {
    package: process.env.APP_ENV === 'production'
      ? 'com.company.myapp'
      : 'com.company.myapp.dev',
  },
  extra: {
    apiUrl: process.env.API_URL,
    eas: { projectId: 'your-project-id' },
  },
})

// Set secrets in EAS (not in .env files)
// eas secret:create --scope project --name API_URL --value https://api.prod.com

Over-the-Air Updates with Expo Updates

EAS Update lets you push JavaScript changes to production users without going through the App Store review process. This is the fastest way to deploy bug fixes — updates are downloaded in the background and applied on the next app launch.

# Push an update to production channel
eas update --branch production --message "Fix checkout crash"

# In your app — check for updates on launch
import * as Updates from 'expo-updates'

async function checkForUpdates() {
  if (!__DEV__) {
    const update = await Updates.checkForUpdateAsync()
    if (update.isAvailable) {
      await Updates.fetchUpdateAsync()
      await Updates.reloadAsync()
    }
  }
}

OTA updates only work for JavaScript changes. Any change to native code (adding a new native module, changing permissions) requires a full store build and submission.

CI/CD with GitHub Actions

name: EAS Build and Submit

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - name: Build production
        run: eas build --platform all --profile production --non-interactive
      - name: Submit to stores
        run: eas submit --platform all --latest --non-interactive
        if: startsWith(github.ref, 'refs/tags/v')

Common Deployment Issues

  • Signing certificate mismatch: Let EAS manage certificates. If you manage them manually, keep them in 1Password or AWS Secrets Manager — losing an iOS distribution certificate means re-signing with a new one and losing push notification history.
  • Build number not incrementing: Set autoIncrement: true in eas.json — stores reject builds with the same build number.
  • OTA update not applying: Check that the runtime version in the update matches the installed app's runtime version. A mismatch silently skips the update.
  • App Store rejection for privacy: All third-party SDKs that access device data must have usage description strings in Info.plist. Missing strings are the #1 cause of App Store rejections.

Want to Build This for Your Team?

I help teams implement the patterns and architectures described in these articles. Let's talk about your project.

Book a Free Call