Relay OTA Docs
Everything you need — from integrating the SDK into your React Native app to running your own self-hosted OTA server.
Install SDK
1 package + peer deps
Native setup
iOS & Android wiring
Push releases
CLI or dashboard upload
Create an app & get your App ID
Every React Native app needs an App record in the dashboard. The App ID is used by the SDK to check for updates.
Go to Apps → New App, choose your platform, enter your bundle ID, and set the runtimeVersion (must match your native build — e.g. 1.0).
Copy the App ID from the app detail page — you'll need it in step 4.
Create an API token
The CLI needs a token to publish releases. Tokens are org-scoped.
Go to Settings → API Tokens → New Token. Give it a name like ci-deploy and store the token — it won't be shown again.
Install the SDK
Install the npm package and its required peer dependencies.
npm install @relay-ota/react-native react-native-fs @react-native-async-storage/async-storage# iOS — link native modules
cd ios && pod installAdd the native bundle loader
The native module intercepts the JS bundle load so the updated bundle is used after an OTA restart.
Copy OtaBundleUpdater.h and OtaBundleUpdater.m from the SDK's src/native/ folder into your Xcode project.
AppDelegate.mm
#import "OtaBundleUpdater.h"
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
return [OtaBundleUpdater bundleURLForBridge:bridge];
}AppDelegate.swift), wrap the method in an @objc extension and import the header via the bridging header.Wrap your app with OtaProvider
OtaProvider holds update state, auto-checks on foreground, and exposes hooks to all children.
// App.tsx
import { OtaProvider, createRNFSFileSystemAdapter } from '@relay-ota/react-native';
const fileSystem = createRNFSFileSystemAdapter();
export default function App() {
return (
<OtaProvider
fileSystem={fileSystem}
config={{
serverUrl: 'https://your-ota-server.com',
appId: 'YOUR_APP_ID',
channel: 'production',
platform: 'IOS',
currentVersion: '1.0.0',
runtimeVersion: '1.0',
}}
>
{/* rest of your app */}
</OtaProvider>
);
}Config reference
serverUrlBase URL of your OTA server (no trailing slash)appIdApp ID from the dashboardchannel"production" or "staging" (created automatically)platform"IOS" or "ANDROID"currentVersionSemver of the JS bundle currently runningruntimeVersionMatches the native binary — must align with the releasecheckOnForegroundAuto-check when app returns to foreground (default: true)Handle updates in your UI
Use the built-in hooks to show update prompts, progress bars, or auto-apply silently.
Option A — useAutoUpdate (recommended)
import { useAutoUpdate } from '@relay-ota/react-native';
export function UpdateHandler() {
const { isReady, isLoading, applyUpdate } = useAutoUpdate();
if (isLoading) return <ActivityIndicator />;
if (isReady) {
return (
<View style={styles.banner}>
<Text>Update ready — restart to apply</Text>
<Button onPress={applyUpdate} title="Restart" />
</View>
);
}
return null;
}Option B — useOtaUpdate (full manual control)
import { useOtaUpdate } from '@relay-ota/react-native';
export function UpdateScreen() {
const {
status, progress, release,
hasUpdate, isLoading, isReady,
checkForUpdates, downloadUpdate, applyUpdate, rollback,
} = useOtaUpdate();
return (
<View>
<Text>Status: {status}</Text>
{hasUpdate && <Button onPress={downloadUpdate} title={`Download v${release?.version}`} />}
{isLoading && <ProgressBar progress={progress} />}
{isReady && <Button onPress={applyUpdate} title="Restart & apply" />}
</View>
);
}UpdateStatus values
idleNo check started yetcheckingPolling the serverup_to_dateAlready on latestupdate_availableNew bundle available — call downloadUpdate()downloadingBundle download in progressreadyBundle saved — call applyUpdate() to restarterrorCheck state.error for detailsPush your first release
Build your JS bundle and upload it via the CLI.
# Install the CLI
npm install -g @relay-ota/cli
# Authenticate
ota login --token YOUR_API_TOKEN
# Bundle for iOS
npx react-native bundle \
--platform ios --dev false \
--entry-file index.js \
--bundle-output ./build/main.jsbundle
# Push to production channel
ota release push \
--app-id YOUR_APP_ID \
--channel production \
--platform ios \
--bundle ./build/main.jsbundle \
--runtime-version 1.0runtimeVersion to the same value used in OtaProvider. The SDK only applies releases whose runtime version matches the running native binary.You're live
Monitor update adoption in Analytics. Use Disable on a release or call rollback() from the SDK to revert if needed.
Troubleshooting
Native module 'OtaBundleUpdater' not found
The native files weren't registered. For iOS: confirm the .m file is added to the Xcode target and pod install ran. For Android: verify OtaBundleUpdaterPackage is added in getPackages() and the package name matches your source directory.
Update downloads but app doesn't restart
Call applyUpdate() after isReady is true. In dev builds, reload() uses DevSettings — confirm Metro is reachable. In production the native reload() call restarts the JS runtime.
runtimeVersion mismatch
The SDK only applies releases where runtimeVersion matches exactly. Ensure the value in OtaProvider config, the CLI --runtime-version flag, and the dashboard App record are all identical.
Checksum error on download
The bundle file is corrupt or was modified in transit. Re-bundle and push a new release.
Ready to ship your first update?
Create a free account and push your first release in minutes.
Get started free