relay-ota-react-native
v0.1.2
Published
Relay OTA SDK for React Native — ship updates without app store review
Maintainers
Readme
relay-ota-react-native
Over-the-air updates for React Native — ship JS bundle changes instantly, without App Store review.
Drop-in replacement for the retired Microsoft CodePush and an alternative to Expo EAS Updates.
Website: relayota.com · Docs: relayota.com/docs · GitHub: relay-ota-react-native
What is Relay OTA?
Relay OTA lets you push JavaScript bundle updates directly to your users' devices — no App Store or Play Store review needed.
- Zero review wait — critical fixes reach users in seconds
- One-click rollback — bad update? revert instantly from the dashboard
- Channels — separate
productionandstagingenvironments - Staged rollouts — release to a percentage of users before going 100%
- Mandatory updates — auto-apply security patches without user interaction
- Analytics — track check, download, and apply rates per release
Works with Expo managed workflow and bare React Native (0.72+).
Installation
npm install relay-ota-react-native @react-native-async-storage/async-storageExpo projects
npx expo install expo-file-systemBare React Native projects
npm install react-native-fs
cd ios && pod installQuick start — Expo
import {
OtaProvider,
useOtaUpdate,
createExpoFileSystemAdapter,
} from "relay-ota-react-native";
const fs = createExpoFileSystemAdapter();
export default function App() {
return (
<OtaProvider
fileSystem={fs}
config={{
serverUrl: "https://ota.yourcompany.com", // your Relay OTA server URL
appId: "YOUR_APP_ID",
channel: "production",
platform: "IOS", // or 'ANDROID'
currentVersion: "1.0.0",
runtimeVersion: "1.0.0",
}}
>
<YourNavigator />
</OtaProvider>
);
}Show an update prompt anywhere in your app:
import { useOtaUpdate } from "relay-ota-react-native";
import { View, Text, Button } from "react-native";
export function UpdateBanner() {
const {
hasUpdate,
isReady,
isLoading,
progress,
downloadUpdate,
applyUpdate,
} = useOtaUpdate();
if (hasUpdate)
return (
<View>
<Text>New version available</Text>
<Button title="Download" onPress={downloadUpdate} />
</View>
);
if (isLoading) return <Text>Downloading… {Math.round(progress * 100)}%</Text>;
if (isReady)
return (
<View>
<Text>Update ready</Text>
<Button title="Restart now" onPress={applyUpdate} />
</View>
);
return null;
}Quick start — Bare React Native
import {
OtaProvider,
createRNFSFileSystemAdapter,
} from "relay-ota-react-native";
const fs = createRNFSFileSystemAdapter();
export default function App() {
return (
<OtaProvider
fileSystem={fs}
config={{
serverUrl: "https://ota.yourcompany.com",
appId: "YOUR_APP_ID",
channel: "production",
platform: "ANDROID",
currentVersion: "1.0.0",
runtimeVersion: "1.0.0",
}}
>
<YourNavigator />
</OtaProvider>
);
}Bare React Native requires the OtaBundleUpdater native module — see Native module setup below.
OtaProvider config
| Prop | Type | Required | Description |
| ------------------- | ------------------------ | -------- | ----------------------------------------------------------- |
| serverUrl | string | ✅ | Base URL of your Relay OTA server |
| appId | string | ✅ | App ID from the OTA dashboard |
| channel | string | ✅ | Release channel — e.g. 'production', 'staging' |
| platform | 'IOS' \| 'ANDROID' | ✅ | Target platform |
| currentVersion | string | ✅ | Current JS bundle version |
| runtimeVersion | string | ✅ | Native runtime version — must match the release |
| checkOnForeground | boolean | — | Auto-check when app returns to foreground (default: true) |
| onUpdate | (release) => void | — | Called when a new update is found |
| onError | (error: Error) => void | — | Called on check/download errors |
Hooks
useOtaUpdate
Full update state and actions:
const {
status, // UpdateStatus
progress, // number 0–100 during download
release, // release metadata or null
error, // Error | null
localBundlePath, // path to downloaded bundle or null
isLoading, // checking or downloading
hasUpdate, // status === 'update_available'
isReady, // status === 'ready'
checkForUpdates, // () => Promise<void>
downloadUpdate, // () => Promise<void>
applyUpdate, // () => Promise<void> — reloads the app
rollback, // () => Promise<void> — removes staged bundle, reloads
} = useOtaUpdate();UpdateStatus values
| Status | Meaning |
| ------------------ | --------------------------------------- |
| idle | No check has run yet |
| checking | Checking for an update |
| up_to_date | No update available |
| update_available | Update found, not downloaded |
| downloading | Download in progress |
| ready | Downloaded, waiting for applyUpdate() |
| error | Last operation failed — see error |
useAutoUpdate
Automatically downloads mandatory updates. Optionally applies on next foreground.
import { useAutoUpdate } from "relay-ota-react-native";
const { isReady, applyUpdate } = useAutoUpdate({ applyImmediately: false });| Option | Type | Default | Description |
| ------------------ | --------- | ------- | ---------------------------------------------- |
| applyImmediately | boolean | false | Auto-call applyUpdate() when bundle is ready |
Rollback
const { rollback } = useOtaUpdate();
// Clears staged bundle and reloads — app falls back to bundled JS
<Button title="Rollback" onPress={rollback} />;Channels
// Staging builds
config={{ ..., channel: 'staging' }}
// Production builds
config={{ ..., channel: 'production' }}Releases are scoped per-channel on the server — staging users never receive production releases.
Push a release (CLI)
# Build the JS bundle
npx react-native bundle \
--platform ios \
--dev false \
--bundle-output ./main.jsbundle
# Push to your OTA server
ota release push \
--channel production \
--bundle ./main.jsbundleNative module setup (bare React Native only)
Expo managed workflow: skip — the Expo filesystem adapter handles everything.
iOS
- Copy
OtaBundleUpdater.handOtaBundleUpdater.mfromnode_modules/relay-ota-react-native/src/native/into your Xcode project - Run
pod install
Android
- Copy
OtaBundleUpdaterModule.ktandOtaBundleUpdaterPackage.ktintoandroid/app/src/main/java/<your-package>/ - Register in
MainApplication.kt:
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
add(OtaBundleUpdaterPackage())
}TypeScript types
import type {
OtaConfig,
UpdateStatus,
UpdateState,
IFileSystemAdapter,
INativeBundleUpdater,
Platform,
} from "relay-ota-react-native";OtaConfig
interface OtaConfig {
serverUrl: string;
appId: string;
channel: string;
platform: "IOS" | "ANDROID";
currentVersion: string;
runtimeVersion: string;
checkOnForeground?: boolean;
onUpdate?: (release: Release) => void;
onError?: (error: Error) => void;
}IFileSystemAdapter
Implement this to bring your own filesystem layer:
interface IFileSystemAdapter {
getDocumentDirectory(): string;
exists(path: string): Promise<boolean>;
readAsString(path: string, encoding: "utf8" | "base64"): Promise<string>;
writeAsString(
path: string,
content: string,
encoding: "utf8" | "base64",
): Promise<void>;
deleteFile(path: string): Promise<void>;
makeDirectory(path: string): Promise<void>;
}Low-level API
import {
checkForUpdate, // (serverUrl, request) => Promise<OtaCheckResponse>
downloadBundle, // (release, fs, onProgress) => Promise<string>
OtaUpdater, // Class — manages the full state machine
} from "relay-ota-react-native";Migrating from CodePush
Microsoft retired CodePush in 2025. Relay OTA uses the same update model and a compatible SDK surface. Key differences:
| | CodePush | Relay OTA | | --------------- | ------------------ | ------------------ | | Hosting | Vendor (shut down) | Relay OTA platform | | Rollback | Yes | Yes | | Analytics | Basic | Built-in, detailed | | Staged rollouts | Yes | Yes |
