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 in | Created by | Used by | |
|---|---|---|---|
| Channel SDK key | Your native build (or the Expo plugin) | ota init | The app, to ask for updates |
ota.config.js | Your repo, committed | ota init | The 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
| Setup | Where it goes | Name |
|---|---|---|
| Expo | app.json | plugins → deploymentKey |
| iOS (bare) | ios/<App>/Info.plist | OtaDeploymentKey |
| Android (bare) | res/values/strings.xml | ota_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
| Setting | iOS | Android | Expo plugin |
|---|---|---|---|
| Channel name | OtaChannel | ota_channel | channel |
| Server URL | OtaServerUrl | ota_server_url | serverUrl |
You almost certainly need neither
Baked into the binary
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:
| Setup | How |
|---|---|
| Expo | Use app.config.js and read the key from an environment variable: { deploymentKey: process.env.OTA_SDK_KEY }. |
| Android | Give 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. |
| iOS | Give 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:
module.exports = {
projectSlug: 'my-app',
runtimeVersion: { policy: 'appVersion' },
channels: {
production: { mandatory: false, rollout: 100 },
staging: { mandatory: false, rollout: 100 },
},
platforms: ['android', 'ios'],
};| Field | Meaning |
|---|---|
projectSlug | Which 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. |
platforms | Which platforms a release builds by default. --platform overrides it for one run. |
channels | Per-channel defaults, written for future use. Flags on the command are what apply today. |
Environment variables
| Variable | Used by | Meaning |
|---|---|---|
OTA_API_KEY | CLI | Authenticate without a login. The way CI publishes. |
OTA_SERVER_URL | CLI | Point at a non-default server. |
OTA_DEBUG | CLI | Set 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.