Documentation

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

1

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 AppsNew 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.

2

Create an API token

The CLI needs a token to publish releases. Tokens are org-scoped.

Go to Settings → API TokensNew Token. Give it a name like ci-deploy and store the token — it won't be shown again.

3

Install the SDK

Install the npm package and its required peer dependencies.

bash
npm install @relay-ota/react-native react-native-fs @react-native-async-storage/async-storage
bash
# iOS — link native modules
cd ios && pod install
4

Add 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

objc
#import "OtaBundleUpdater.h"

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
  return [OtaBundleUpdater bundleURLForBridge:bridge];
}
If your project uses Swift/Objective-C bridging (AppDelegate.swift), wrap the method in an @objc extension and import the header via the bridging header.
5

Wrap your app with OtaProvider

OtaProvider holds update state, auto-checks on foreground, and exposes hooks to all children.

tsx
// 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 dashboard
channel"production" or "staging" (created automatically)
platform"IOS" or "ANDROID"
currentVersionSemver of the JS bundle currently running
runtimeVersionMatches the native binary — must align with the release
checkOnForegroundAuto-check when app returns to foreground (default: true)
6

Handle updates in your UI

Use the built-in hooks to show update prompts, progress bars, or auto-apply silently.

Option A — useAutoUpdate (recommended)

tsx
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)

tsx
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 yet
checkingPolling the server
up_to_dateAlready on latest
update_availableNew bundle available — call downloadUpdate()
downloadingBundle download in progress
readyBundle saved — call applyUpdate() to restart
errorCheck state.error for details
7

Push your first release

Build your JS bundle and upload it via the CLI.

bash
# 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.0
Set runtimeVersion 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