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.
{
"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:
export default {
expo: {
plugins: [
['@otaupdate/react-native', { deploymentKey: process.env.OTA_SDK_KEY }],
],
},
};Run prebuild
npx expo prebuild --cleanConfirm the plugin wrote its configuration:
grep ota_ android/app/src/main/res/values/strings.xml
# <string name="ota_deployment_key" translatable="false">ota_live_…</string>Build a release build
npx expo run:android --variant release
npx expo run:ios --configuration ReleaseEAS Build runs prebuild for you — no extra step there.
Expo Go will not work
isNativeModuleAvailable is false.Plugin options
| Option | Required | Meaning |
|---|---|---|
deploymentKey | Yes | The channel SDK key from `ota init`. Used for both platforms. |
channel | No | Only for builds still carrying a project-wide key. A channel key already names its channel and cannot be overridden from config. |
serverUrl | No | Points the build at a non-default endpoint. Internal use — leave it unset. |
iosDeploymentKey, androidDeploymentKey | No | Legacy 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
NativeModules.OtaUpdate is undefined and every SDK call throws.