Configuration Reference

Every configuration option for the OTA Update SDK and CLI: channel SDK keys, ota.config.js, runtime versions and environment variables.

There are exactly two pieces of configuration, and neither is a secret you must hide.

Lives inCreated byUsed by
Channel SDK keyYour native build (or the Expo plugin)ota initThe app, to ask for updates
ota.config.jsYour repo, committedota initThe CLI, to publish

Your publishing credentials are the third piece, and they are the one secret — they live in your CLI profile or in OTA_API_KEY in CI, never in the repo.

The channel SDK key, per platform

SetupWhere it goesName
Expoapp.jsonplugins → deploymentKey
iOS (bare)ios/<App>/Info.plistOtaDeploymentKey
Android (bare)res/values/strings.xmlota_deployment_key

Keys are prefixed by channel — ota_live_… for production, ota_test_… for every other channel — so a wrong paste is visible on sight. One key serves both platforms.

Optional native settings

SettingiOSAndroidExpo plugin
Channel nameOtaChannelota_channelchannel
Server URLOtaServerUrlota_server_urlserverUrl

You almost certainly need neither

A channel key already names its channel, and the request cannot override it — that is what stops a staging build from pulling production updates. The channel setting only matters for builds carrying an older project-wide key. The server URL defaults to the hosted platform; set it only for an internal deployment.

Baked into the binary

Native config is read once at startup and is not itself OTA-updatable. If you rotate a channel's key, every already-shipped build carrying the old one stops receiving updates until users install a new build from the store.

Building staging and production from one project

The key is per channel, so each build variant needs its own value. Rather than editing a file before each build, let the build pick it:

SetupHow
ExpoUse app.config.js and read the key from an environment variable: { deploymentKey: process.env.OTA_SDK_KEY }.
AndroidGive each product flavour or build type its own res/values/strings.xml, or set com.otaupdate.DEPLOYMENT_KEY as a manifest <meta-data> from a Gradle manifestPlaceholder.
iOSGive each Xcode configuration its own Info.plist value, or drive it from a build setting.

On Android every setting has a manifest equivalent, which is what makes the Gradle route possible: com.otaupdate.DEPLOYMENT_KEY, com.otaupdate.CHANNEL and com.otaupdate.SERVER_URL. A string resource wins when both are present.

ota.config.js

Written by ota init, committed, and read by every publishing command:

ota.config.js
module.exports = {
  projectSlug: 'my-app',
  runtimeVersion: { policy: 'appVersion' },
  channels: {
    production: { mandatory: false, rollout: 100 },
    staging: { mandatory: false, rollout: 100 },
  },
  platforms: ['android', 'ios'],
};
FieldMeaning
projectSlugWhich project to publish to. Everything else about the app is inferred.
runtimeVersion{ policy: 'appVersion' } takes the version from app.json / package.json. Use { explicit: '1.4.0' } to pin it yourself.
platformsWhich platforms a release builds by default. --platform overrides it for one run.
channelsPer-channel defaults, written for future use. Flags on the command are what apply today.

Environment variables

VariableUsed byMeaning
OTA_API_KEYCLIAuthenticate without a login. The way CI publishes.
OTA_SERVER_URLCLIPoint at a non-default server.
OTA_DEBUGCLISet to 1 for full stack traces.

What the device sends

Every update check posts the channel SDK key, the platform, the native app version, the label and hash of the bundle it is running, the native bundle identifier, and a random per-install UUID. Nothing else — no hardware identifier, no advertising ID, no user data. The bundle identifier is checked against the one registered on the project, so a key lifted out of your app cannot be used to pull your bundles into a different app.