Expo OTA Updates Setup

Configure over-the-air updates in an Expo project with the OTA Update config plugin: one channel key, prebuild, and EAS Build notes.

The config plugin writes every native change during prebuild. You do not edit native files by hand.

Add the plugin to app.json

Paste the key for the channel this build should follow — the staging key in a staging build, the production key in your store build. One key covers both iOS and Android, and it is the only thing the plugin needs; the SDK already knows which server to talk to.

app.json
{
  "expo": {
    "plugins": [
      [
        "@otaupdate/react-native",
        { "deploymentKey": "ota_live_xxxxxxxxxxxxxxxxxxxxxxxx" }
      ]
    ]
  }
}

Building staging and production from the same app.json? Use app.config.js and pick the key from an environment variable, so the value is chosen by the build rather than by an edit somebody has to remember:

app.config.js
export default {
  expo: {
    plugins: [
      ['@otaupdate/react-native', { deploymentKey: process.env.OTA_SDK_KEY }],
    ],
  },
};

Run prebuild

terminal
npx expo prebuild --clean

Confirm the plugin wrote its configuration:

terminal
grep ota_ android/app/src/main/res/values/strings.xml
# <string name="ota_deployment_key" translatable="false">ota_live_…</string>

Build a release build

terminal
npx expo run:android --variant release
npx expo run:ios --configuration Release

EAS Build runs prebuild for you — no extra step there.

Expo Go will not work

The native module does not exist in Expo Go. Use a development or release build. In Expo Go the SDK logs a warning and no-ops rather than crashing, and isNativeModuleAvailable is false.

Plugin options

OptionRequiredMeaning
deploymentKeyYesThe channel SDK key from `ota init`. Used for both platforms.
channelNoOnly for builds still carrying a project-wide key. A channel key already names its channel and cannot be overridden from config.
serverUrlNoPoints the build at a non-default endpoint. Internal use — leave it unset.
iosDeploymentKey, androidDeploymentKeyNoLegacy per-platform keys, for apps that predate channel keys. New projects use deploymentKey.

What the plugin does — and deliberately does not do

On Android it writes strings.xml only. It does not touch MainApplication: modern Expo is bridgeless and exposes no ReactNativeHost to override, so the SDK registers a ReactNativeHostHandler — Expo's own extension point, the same one expo-updates uses — that Expo autolinking picks up automatically. If you go looking for a MainApplication edit after prebuild, its absence is correct.

On iOS it writes the Info.plist keys and rewrites the release branch of bundleURL() in your AppDelegate — Swift and Objective-C++ are both handled.

Do not delete react-native.config.js from the package

The SDK is linked by both linkers on purpose: React Native autolinking registers the native module JS talks to, and Expo autolinking registers the bundle-path handler. Expo normally drops a package from RN autolinking when it is also an Expo module; that file opts out of the skip. Without it, NativeModules.OtaUpdate is undefined and every SDK call throws.