Skip to content

Expo OTA (Over-The-Air) Updates Deployment Guide

This guide covers the complete OTA update workflow for the encache mobile app, including publishing updates, monitoring deployments, and rolling back if needed.

Table of Contents

  1. Overview
  2. Prerequisites
  3. Publishing Updates
  4. Monitoring Updates
  5. Rollback Procedures
  6. Troubleshooting
  7. FAQ

Overview

The Expo OTA (Over-The-Air) system allows distributing app updates without requiring app store submission. Updates can be deployed to multiple independent channels (development, staging, production) with immediate global availability.

Key Features

  • No App Store Review: Deploy updates instantly
  • Multiple Channels: Independent development, staging, and production environments
  • Automatic Distribution: Devices receive updates on next launch
  • Rollback Support: Quickly revert to previous versions
  • Version Tracking: Monitor which devices have which version

Architecture

┌─────────────┐
│  Developer  │
│  Pushes OTA │
└──────┬──────┘
       │ eas update --channel <channel>
┌─────────────────────┐
│  EAS Update Service │
│  (Expo's CDN)       │
└──────┬──────────────┘
       │ Distributes via global CDN
┌──────────────────────┐
│  Mobile Devices      │
│  (All channels)      │
│  Check on app launch │
└──────────────────────┘

Prerequisites

Before you can publish OTA updates, ensure the following are completed:

Local Setup

  1. Install Expo CLI

    npm install -g eas-cli
    

  2. Authenticate with Expo

    eas login
    

This creates credentials at ~/.expo/. You'll need an Expo account linked to the encache project.

  1. Install Dependencies

    cd apps/mobile
    npm install
    

  2. Environment Variables

For CI/CD systems, set the EXPO_TOKEN environment variable:

export EXPO_TOKEN="your-token-here"

Get your token:

eas token create --scope admin

Project Configuration

The following files must be properly configured:

  • apps/mobile/app.json: Defines updates.url pointing to EAS Update service
  • apps/mobile/eas.json: Defines build profiles and channels
  • apps/mobile/package.json: Must include expo-updates dependency
  • apps/mobile/index.tsx: Must initialize expo-updates on startup

Verify configuration:

cd apps/mobile

# Validate app.json
node -e "
  const config = require('./app.json');
  console.log('✓ OTA URL:', config.expo.updates.url);
  console.log('✓ Auto-check:', config.expo.updates.checkAutomatically);
"

# Validate eas.json
node -e "
  const config = require('./eas.json');
  console.log('✓ Channels:', config.update.channels.join(', '));
"

Publishing Updates

Quick Start: Publish to Staging

The simplest way to publish an update:

cd apps/mobile

# Publish to staging channel
npm run publish:update

Or manually:

eas update --channel staging --message "Bug fix: crash on launch"

Detailed Publish Workflow

1. Ensure Code is Committed

All changes must be committed to git:

git add .
git commit -m "Fix: resolve memory leak in background task"
git push origin feature/memory-fix

The OTA system records the commit hash with each update for audit purposes.

2. Publish to Staging First

Always test in staging before production:

eas update \
  --channel staging \
  --message "Testing memory fix: commit abc123"

Expected output:

✓ Published update to staging
  Update ID: <uuid>
  Published at: 2026-05-10T12:34:56.789Z

3. Monitor Staging Devices

Check the EAS dashboard to verify: - Update appears in the "staging" channel - Devices subscribed to staging are downloading - No error messages in device logs

# View recent updates
eas update list --channel staging --limit 10

4. Promote to Production

Once staging is validated (typically 30+ minutes of device usage):

eas update \
  --channel production \
  --message "Release v1.2.5: memory optimization"

Advanced: Publishing from CI/CD

The GitHub Actions workflow automatically publishes updates:

  • On push to main: Publishes to staging channel
  • On push to release/*: Publishes to production channel
  • Manual trigger: Allows custom channel and message

Example: Trigger manual release

gh workflow run mobile-release.yml \
  -f channel=production \
  -f message="Hotfix: crash on launch"

Channel Promotion Workflow

Recommended promotion path:

Feature Branch
    ├─→ [development] (developer's local branch)
    ├─→ [staging] (test on staging devices)
    │   │ Wait 30+ minutes for device coverage
    │   │ Monitor error reports
    │   │
    └─→ [production] (release to all users)
        │ Monitor error reports
        │ Be ready to rollback if needed

Monitoring Updates

View Recent Updates

List all updates on a channel:

eas update list --channel staging

Output:

Channel: staging
  ID: <uuid>          Version: v1.2.4       Created: 5 minutes ago
  ID: <uuid>          Version: v1.2.3       Created: 2 hours ago
  ID: <uuid>          Version: v1.2.2       Created: 1 day ago

View Device Status

See which devices have which version:

# Via EAS dashboard (recommended for visual view)
open https://expo.dev

# Via CLI
eas device list
eas update list --channel production --limit 10

Monitor Error Reports

Devices report errors during update checks:

# Monitor error rates
eas update list --channel production

# Check specific update
eas update view <update-id>

Application Metrics

In your app, monitor OTA-related metrics:

// expo-updates does not expose an addListener API.
// Track OTA events by checking the result of checkForUpdateAsync/fetchUpdateAsync:
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
  await Updates.fetchUpdateAsync();
  analytics.track('ota_update_applied', { channel: Updates.channel });
  await Updates.reloadAsync();
}

Rollback Procedures

Scenario: Production Bug After Update

If a production update causes issues:

  1. Assess Impact
  2. How many devices are affected?
  3. Is the impact critical?
  4. Can users work around it?

  5. Decide: Fix or Rollback?

  6. Rollback: If the issue is critical (crashes, data loss) → immediate rollback

  7. Fix: If the issue is minor → prepare hotfix and republish

Immediate Rollback

Revert production to the previous known-good version:

# Find the update group ID to roll back to
eas update list --channel production --limit 5

# Re-publish a previous update group (true rollback — no code checkout needed)
eas update:republish --group <previous-update-group-id> --branch production

The system will: 1. Publish the old update as the new "current" version 2. Within 30 seconds, devices will check for updates 3. Within 5 minutes, 95%+ of devices will have reverted

Hotfix Release

Prepare a hotfix and release:

# Create hotfix branch
git checkout -b hotfix/crash-fix

# Make the fix
# ... edit files ...

# Commit and push
git add apps/mobile/src/**
git commit -m "Hotfix: resolve crash on launch"
git push origin hotfix/crash-fix

# Create PR and merge to main
gh pr create --title "Hotfix: crash" --body "Critical crash fix"

# Once merged to main, the CI pipeline automatically publishes to staging
# After validation, publish to production:
eas update --channel production --message "Hotfix v1.2.4: crash on launch"

Preventing Rollbacks

  1. Test thoroughly before release
  2. Use staging for 30+ minutes before promoting to production
  3. Test on multiple device types

  4. Monitor closely after release

  5. Check error reports 5 minutes after publication
  6. Have team members test the app

  7. Use feature flags

  8. Enable new features gradually using feature flags
  9. Disable problematic features without a full rollback

Troubleshooting

Issue: Update Not Downloading

Symptoms: Devices don't receive updates even after 5+ minutes

Solutions:

  1. Verify device is on correct channel

    // In app code:
    const channel = Updates.channel;
    console.log('Current channel:', channel);
    

  2. Verify network connectivity

  3. Device must have internet access
  4. Check firewall/proxy settings

  5. Check update availability

    eas update list --channel <channel> --limit 1
    

  6. Force update check (in app)

    await Updates.checkForUpdateAsync();
    

Issue: Build Fails During CI/CD

Symptoms: GitHub Actions workflow fails at "Build app" step

Solutions:

  1. Check EAS token is valid

    eas whoami
    

  2. Verify project ID in app.json matches EAS

    eas project info
    

  3. Check for uncommitted changes

    git status
    

  4. View detailed logs

    eas build list --limit 10
    

Issue: "Bundle Size Exceeds Limit"

Symptoms: eas update fails with "bundle exceeds 50MB"

Solutions:

  1. Analyze bundle size

    npm run build:analyze
    

  2. Implement code splitting

    // Lazy-load heavy modules
    const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
    

  3. Remove unused dependencies

    npm audit
    npm prune
    

  4. Compress assets

  5. Optimize images (use WebP)
  6. Remove unused fonts

Issue: "Uncommitted Changes" Error

Symptoms: publish-update.sh fails with "uncommitted changes"

Solutions:

# Commit all changes
git add .
git commit -m "Description of changes"

# Or discard changes
git checkout apps/mobile/
git clean -fd apps/mobile/

Issue: Authentication Fails

Symptoms: eas update fails with "authentication error"

Solutions:

  1. Re-authenticate

    eas logout
    eas login
    

  2. Create new token for CI/CD

    eas token create --scope admin
    # Add to GitHub Secrets as EXPO_TOKEN
    

  3. Verify credentials

    eas whoami
    eas project info
    

FAQ

Q: Can I schedule an update to publish at a specific time?

A: No, EAS Update publishes immediately when eas update runs. To schedule: - Use GitHub Actions scheduled workflows - Trigger via CI/CD at a specific time

Q: How long do devices take to receive an update?

A: - Check interval: Every app launch (configurable via checkAutomatically) - Download time: Usually <1 minute on 4G - Apply time: Next app launch - Global coverage: 95%+ within 5 minutes

Q: Can I target specific devices?

A: No, OTA updates are channel-based. All devices on a channel receive the same version. To target subsets: - Use multiple channels - Use feature flags in app code - Use staged rollout (publish to staging first)

Q: What's the maximum update size?

A: 50MB for the entire update bundle. This includes: - JavaScript code - Assets (images, fonts, sounds) - Dependencies bundled with the app

Measure bundle size:

eas update --simulate

Q: Can I revert a published update?

A: Yes, publish the old version again:

eas update list --channel production --limit 2
eas update --channel production --message "Rollback to previous version"

Q: Do updates require app restart?

A: - First time: No, app reloads without restart (if possible) - Subsequent times: App reloads on next launch

Q: Can I use OTA updates with native code changes?

A: No, OTA updates are JavaScript/asset only. For native changes: - Update native code - Rebuild via eas build - Submit to app stores

Q: How are updates signed and verified?

A: Expo automatically signs and verifies updates: - All updates signed with certificate - Device verifies signature before applying - Corrupted updates are rejected

Q: How much does OTA updates cost?

A: Expo OTA Updates is included with EAS Build. Bandwidth is free for updates (paid separately only if you exceed limits).

Check pricing: https://expo.dev/pricing


Support

For additional help:

  • Expo Documentation: https://docs.expo.dev/deploy/send-over-the-air-updates/
  • EAS Update Docs: https://docs.expo.dev/eas-update/getting-started/
  • Community: https://forums.expo.dev
  • Report Issues: https://github.com/expo/expo/issues

Changelog

v1.0.0 (2026-05-10)

  • Initial OTA setup with development, staging, production channels
  • Automated CI/CD pipeline via GitHub Actions
  • Configuration validation tests
  • Rollback procedures documented