---
modificationDate: April 28, 2026
title: Using EAS Update in an existing native app
description: Learn how to integrate EAS Update into your existing native Android and iOS app to enable over-the-air updates.
---

<AgentInstructions>

## Submitting Feedback

If this page contains errors, outdated information, or gaps that blocked you from completing a task, report it so the docs can be improved:

curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/eas-update/integration-in-existing-native-apps/","feedback":"🤖 Agent feedback: <specific, actionable description>"}'

Only submit when you have something specific and actionable to report.

</AgentInstructions>

# Using EAS Update in an existing native app

Learn how to integrate EAS Update into your existing native Android and iOS app to enable over-the-air updates.

> For the complete documentation index, see [llms.txt](/llms.txt). Use this file to discover all available pages.

> If your project is a **greenfield React Native app** — primarily built with React Native from the start, and the entry point of the app is React Native, then skip this guide and proceed to [Get started with EAS Update](/eas-update/getting-started).

This guide explains how to integrate EAS Update in an existing native app, sometimes referred to as a brownfield app. It assumes that you are using Expo SDK 52 or later, and React Native 0.76 or later.

Instructions are not available for older Expo SDK and React Native versions. Additional hands-on support for integrating with older versions can only be provided for enterprise customers ([contact us](https://expo.dev/contact)).

> The following instructions may not work for all projects. The specifics of integrating EAS Update into existing projects depend heavily on the specifics of your app, and so you may need to adapt the instructions to your unique setup. If you encounter issues, [create an issue on GitHub](https://github.com/expo/expo/issues) or open a pull request to suggest improvements to this guide.

Prerequisites

7 requirements

1.

A brownfield native project with React Native

You should have a brownfield native project with React Native installed and configured to render a root view. If you don't have this yet, follow the [Integration with Existing Apps](https://reactnative.dev/docs/integration-with-existing-apps) guide from the React Native documentation and then come back here.

2.

Latest Expo SDK and supported React Native

Your app must be using the [latest Expo SDK version and its supported React Native version](/versions/latest#each-expo-sdk-version-depends-on-a-react-native-version).

3.

No other update libraries

Remove any other update library integration from your app, such as `react-native-code-push`, and ensure that your app compiles and runs successfully in both debug and release on your supported platforms.

4.

Expo modules installed

Support for Expo modules (through the `expo` package) must be installed and configured in your project. See [Integrating Expo tools into existing native apps](/brownfield/overview) for more information.

5.

metro.config.js extends expo/metro-config

Your **metro.config.js** [must extend `expo/metro-config`](/guides/customizing-metro#customizing).

6.

babel.config.js extends babel-preset-expo

Your **babel.config.js** [must extend `babel-preset-expo`](/versions/latest/config/babel).

7.

`npx expo export` runs successfully

The command `npx expo export -p android` must run successfully in your project if it supports Android, and `npx expo export -p ios` if it supports iOS.

## Installation and basic configuration

Follow steps 1, 2, 3, and 4 from the [Get started with EAS Update](/eas-update/getting-started) guide.

After this is complete, you will have installed and authenticated with `eas-cli`, installed `expo-updates` to your project, initialized an associated EAS project, and added basic configuration to your native projects.

## Opt out of automatic setup

The next step is to disable the default behavior of `expo-updates` to automatically set itself up in a way that supports greenfield React Native projects.

### Disable automatic setup on Android

Modify **android/gradle.properties** to set the property that disables automatic updates initialization, as in the example below:

### Disable automatic setup on iOS

Pass in the environment variable to CocoaPods installation to disable automatic updates initialization.

```sh
EX_UPDATES_CUSTOM_INIT=1 npx pod-install
```

## Set up your React Native app to use expo-updates for loading the release bundle

The next step is to integrate `expo-updates` into your Android and iOS projects so that your app will use `expo-updates` as the source of your app JavaScript in release builds.

### Integrating expo-updates with your React Native bundling

1.  Ensure that your Metro config extends the Expo config, as in this example:
    
    ```js
    // Learn more https://docs.expo.dev/guides/customizing-metro
    const { getDefaultConfig } = require('expo/metro-config');
    
    /** @type {import('expo/metro-config').MetroConfig} */
    const config = getDefaultConfig(__dirname); // eslint-disable-line no-undef
    
    // Make any custom changes you need for your project by
    // directly modifying "config"
    
    module.exports = config;
    ```
    
2.  If you are using a custom entry point, be sure to include Expo initialization there. This ensures that Expo libraries (including `expo-updates`) are all initialized properly. Here are two examples:
    
    ```jsx
    // Expo recommends using registerRootComponent().
    // It registers the component with the react-native AppRegistry,
    // and performs all required Expo initialization
    // (including expo-updates setup)
    
    import App from './App';
    import { registerRootComponent } from 'expo';
    
    registerRootComponent(App);
    ```
    
    ```jsx
    // If you need to keep an existing entry point that uses AppRegistry directly,
    // you will need to add a call to Expo's initialization before registering the
    // app, as shown below.
    import App from './App';
    import 'expo/src/Expo.fx';
    import { AppRegistry } from 'react-native';
    
    function getApp() {
      return <App />;
    }
    
    AppRegistry.registerComponent('App', () => getApp());
    ```
    

### Integrating expo-updates on Android

The following instructions assume you have an app written in Kotlin. You will need to update two files: **MainApplication.kt** and **MainActivity.kt**.

#### MainApplication changes

Open **android/app/src/main/java/com/<your-app-name>/MainApplication.kt** and follow the steps below.

1.  Your application class should implement `ReactApplication`.
2.  Override `reactHost` to use `ExpoReactHostFactory.getDefaultReactHost()`. This sets up the React host with proper expo-updates integration.
3.  In `onCreate()`, call `loadReactNative()` and `ApplicationLifecycleDispatcher.onApplicationCreate()` to initialize Expo modules.

```kotlin
package com.yourpackagename

import android.app.Application
import android.content.res.Configuration

import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.ReactHost
import com.facebook.react.common.ReleaseLevel
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint

import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ExpoReactHostFactory

// Step 1
class MainApplication : Application(), ReactApplication {

  // Step 2
  override val reactHost: ReactHost by lazy {
    ExpoReactHostFactory.getDefaultReactHost(
      context = applicationContext,
      packageList =
        PackageList(this).packages.apply {
          // Packages that cannot be autolinked yet can be added manually here, for example:
          // add(MyReactNativePackage())
        }
    )
  }

  // Step 3
  override fun onCreate() {
    super.onCreate()
    DefaultNewArchitectureEntryPoint.releaseLevel = try {
      ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
    } catch (e: IllegalArgumentException) {
      ReleaseLevel.STABLE
    }
    loadReactNative(this)
    ApplicationLifecycleDispatcher.onApplicationCreate(this)
  }

  override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
  }
}
```

#### MainActivity changes

Open **android/app/src/main/java/com/<your-app-name>/MainActivity.kt** and follow the steps below.

1.  Your React Native activity should subclass `com.facebook.react.ReactActivity`.
2.  Override `getMainComponentName()` to return the name of the app you registered in your JS entry point above.
3.  Override `createReactActivityDelegate()` using `ReactActivityDelegateWrapper` as shown below.

```kotlin
package com.yourpackagename

import android.os.Bundle

import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate

import expo.modules.ReactActivityDelegateWrapper

// Step 1
class MainActivity : ReactActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(null)
  }

  // Step 2
  override fun getMainComponentName(): String = "App"

  // Step 3
  override fun createReactActivityDelegate(): ReactActivityDelegate {
    return ReactActivityDelegateWrapper(
      this,
      BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
      object : DefaultReactActivityDelegate(
        this,
        mainComponentName,
        fabricEnabled
      ) {})
  }
}
```

### Integrating expo-updates on iOS

The following instructions assume you have an app written in Swift, with one or more native screens that have custom UIViewControllers. We will add a custom view controller that renders your React Native app.

#### AppDelegate changes

1.  Modify **AppDelegate.swift** so that it extends `ExpoAppDelegate`.
2.  If you are not already doing so, add a public method to get the running `AppDelegate` instance, so that your custom view controller can access it later.
3.  Add a reference to the singleton instance of the `expo-updates` `AppController` class, which manages the updates system on iOS.
4.  Add a new class, `CustomReactNativeFactoryDelegate`, that extends `ExpoReactNativeFactoryDelegate` and overrides the `bundleUrl()` method to return the correct bundle URL for updates, if the updates system is running.
5.  The `didFinishLaunchingWithOptions()` method needs to perform two steps:
    1.  Initialize the `ExpoReactNativeFactory` using the `CustomReactNativeFactoryDelegate` created above. This will be used later to create the React Native root view.
    2.  Call `AppController.initializeWithoutStarting()`. This creates the controller instance, but defers the rest of the updates startup procedure until it is needed.

```swift
import Expo
import EXUpdates
import React
import ReactAppDependencyProvider
import UIKit

@UIApplicationMain
// Step 1
class AppDelegate: ExpoAppDelegate {
  var launchOptions: [UIApplication.LaunchOptionsKey: Any]?

  // Step 2
  public static func shared() -> AppDelegate {
    guard let delegate = UIApplication.shared.delegate as? AppDelegate else {
      fatalError("Could not get app delegate")
    }
    return delegate
  }

  // Step 3
  var updatesController: (any InternalAppControllerInterface)?

  // Step 5
  private func initializeReactNativeAndUpdates(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
    // Step 5.1
    self.launchOptions = launchOptions
    let delegate = CustomReactNativeFactoryDelegate()
    let factory = ExpoReactNativeFactory(delegate: delegate)
    delegate.dependencyProvider = RCTAppDependencyProvider()

    reactNativeFactoryDelegate = delegate
    reactNativeFactory = factory
    // Step 5.2
    AppController.initializeWithoutStarting()
  }

  /**
   Application launch initializes the custom view controller: all React Native
   and updates initialization is handled there
   */
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    initializeReactNativeAndUpdates(launchOptions)

    // Create custom view controller, where the React Native view will be created
    self.window = UIWindow(frame: UIScreen.main.bounds)
    let controller = CustomViewController()
    controller.view.clipsToBounds = true
    self.window?.rootViewController = controller
    window?.makeKeyAndVisible()

    return true
  }

  override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    return super.application(app, open: url, options: options) ||
      RCTLinkingManager.application(app, open: url, options: options)
  }
}

// Step 4
class CustomReactNativeFactoryDelegate: ExpoReactNativeFactoryDelegate {
  let bundledUrl = Bundle.main.url(forResource: "main", withExtension: "jsbundle")
  override func sourceURL(for bridge: RCTBridge) -> URL? {
    // needed to return the correct URL for expo-dev-client.
    bridge.bundleURL ?? bundleURL()
  }

  override func bundleURL() -> URL? {
    if let updatesUrl = AppDelegate.shared().updatesController?.launchAssetUrl() {
      return updatesUrl
    }
    return bundledUrl
  }
}
```

#### Implementing a custom view controller

1.  The view controller should implement the updates protocol `AppControllerDelegate`.
2.  The view controller initialization should
    1.  Set the app delegate's updates controller instance, so that its `bundleURL()` method above works correctly for updates.
    2.  Set the `AppController` delegate to the view controller instance
    3.  Start the `AppController`
3.  Finally, the view controller must implement the one method in the `AppControllerDelegate` protocol, `appController(_ appController: AppControllerInterface, didStartWithSuccess success: Bool)`. This method will be called once the updates system is fully initialized, and the latest update (or the embedded bundle) is ready to be rendered.
    1.  Create the React Native root view using the `ExpoReactNativeFactory` created by the app delegate. The app name passed in must match the app name that you registered in your JS entry point above.
    2.  Add this root view to the view controller.

```swift
import UIKit
import EXUpdates
import ExpoModulesCore

/**
 Custom view controller that handles React Native and expo-updates initialization
 */
// Step 1
public class CustomViewController: UIViewController, AppControllerDelegate {
  let appDelegate = AppDelegate.shared()

  // Step 2
  public convenience init() {
    self.init(nibName: nil, bundle: nil)
    self.view.backgroundColor = .clear
    // Step 2.1
    appDelegate.updatesController = AppController.sharedInstance
    // Step 2.2
    AppController.sharedInstance.delegate = self
    // Step 2.3
    AppController.sharedInstance.start()
  }

  required public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  }

  @available(*, unavailable)
  required public init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  // Step 3
  public func appController(
    _ appController: AppControllerInterface,
    didStartWithSuccess success: Bool
  ) {
    createView()
  }

  private func createView() {
    // Step 3.1
    guard let rootViewFactory: RCTRootViewFactory = appDelegate.reactNativeFactory?.rootViewFactory else {
      fatalError("rootViewFactory has not been initialized")
    }
    let rootView = rootViewFactory.view(
      withModuleName: "main",
      initialProperties: [:],
      launchOptions: appDelegate.launchOptions
    )
    // Step 3.2
    let controller = self
    controller.view.clipsToBounds = true
    controller.view.addSubview(rootView)
    rootView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      rootView.topAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.topAnchor),
      rootView.bottomAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.bottomAnchor),
      rootView.leadingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.leadingAnchor),
      rootView.trailingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.trailingAnchor)
    ])
  }
}
```

## Common questions

How long will this take to add to my app?

Assuming you are using the latest version of React Native supported by the Expo SDK, and you are comfortable with the React Native integration in your native projects, then you can likely integrate EAS Update in a similar amount of time as it would take you to integrate with a tool like CodePush or Sentry.

The most important factor is the React Native version that your app uses. If your app uses anything older than the latest supported version by the Expo SDK (as referenced at the top of this guide), then you will want to upgrade to that version first, and the time that will take is heavily dependent on the size and complexity of the app and skill and experience level of the team working on it.

I'm migrating from CodePush, what else do I need to know?

To learn more, see [Migrating from CodePush](/eas-update/codepush) guide.
