@codemagic/react-native-patch
v0.5.0
Published
React Native client SDK for Codemagic Patch — over-the-air JavaScript & asset updates for React Native and Expo apps.
Readme
@codemagic/react-native-patch
Client SDK for Codemagic Patch: a self-hosted CodePush successor for over-the-air updates on React Native and Expo apps. Download, verify, and boot JS/asset bundles on device so you can ship JS changes without a new store binary.
This package is only the on-device side. You also need a Patch server (self-hosted) and the cmpatch CLI to publish releases. The monorepo has both, plus a dashboard and a local evaluation stack.
What you get
From the monorepo:
- Full stack via Docker Compose (API, worker, Postgres, MinIO, dashboard)
- Web dashboard with RBAC and release management
- On-device demo app for trying OTA updates quickly
From this SDK:
Binary patches when available, for smaller downloads (falls back to a full bundle)
Signature verification when you configure a public key
Automatic rollback if a new package fails before the app reports ready
Expo config plugin for prebuild projects, plus bare React Native wiring
One-line
Patch.wrap(App)root wrapper, CodePush-stylesync(), or step-by-step APIs when you need control
Requirements
- React Native
0.73+— New Architecture support starts at RN 0.76; RN 0.73–0.75 are supported on the Old (Paper) Architecture only - Android
minSdkVersion23+ (the library follows the host'srootProject.ext.minSdkVersion; without one it builds at 23) - Android native build with CMake/JNI support
- iOS native build with CocoaPods and mixed Swift/ObjC++ compilation
- Expo SDK 52+ (via the bundled config plugin — see Expo apps)
- Expo Go is not supported — the native module is not part of the Expo Go runtime
Installation
npm install @codemagic/react-native-patch
# or
yarn add @codemagic/react-native-patchreact (>=18) and react-native (>=0.73) are peer dependencies. On iOS, install the native pod:
cd ios && pod installQuick start
Configuration lives in native resources (deployment key, API URL, download base URL, optional public key), not in a JS config object. The Expo plugin writes those for you; bare apps set them in Info.plist / strings.xml and wire bundle selection.
Expo (config plugin)
If you maintain native projects manually, follow manual native setup.
{
"plugins": [
["@codemagic/react-native-patch", {
"ios": {
"deploymentKey": "<key>",
"downloadBaseUrl": "<url>",
"apiUrl": "<url>",
"publicKey": "<pem>"
},
"android": {
"deploymentKey": "<key>",
"downloadBaseUrl": "<url>",
"apiUrl": "<url>",
"publicKey": "<pem>"
}
}]
]
}Configure at least one platform. Every block you provide needs deploymentKey, downloadBaseUrl, and apiUrl (publicKey and maxLaunchAttempts are optional). Then run expo prebuild. See Configuration for where each value comes from.
Bare React Native
Add CodemagicPatchDeploymentKey, CodemagicPatchApiUrl, and CodemagicPatchDownloadBaseUrl (optional CodemagicPatchPublicKey) to Android strings.xml and iOS Info.plist, then point RN’s JS bundle path at CodemagicPatch.getJSBundleFile / CodemagicPatch.bundleURL() so boot order is pending package → current package → embedded bundle. Full snippets are in Configuration below.
Wrap the root component
import * as Patch from "@codemagic/react-native-patch";
function App() {
return <AppNavigator />;
}
export default Patch.wrap(App);wrap() renders your app right away and runs sync() once the root has mounted. With no options, ordinary updates install on the next restart and mandatory updates install immediately; pass WrapOptions as the second argument to change that (Patch.wrap(App, { installMode: Patch.InstallMode.ON_NEXT_RESUME })).
Set checkFrequency: "ON_APP_RESUME" to check on mount and on foreground return; the default "ON_APP_START" checks only on mount. For example: Patch.wrap(App, { checkFrequency: Patch.CheckFrequency.ON_APP_RESUME, installMode: Patch.InstallMode.ON_NEXT_RESTART }). For manual checks, use the direct APIs without wrap(). Patch.CheckFrequency and Patch.InstallMode provide named constants; existing string literals remain supported.
wrap() forwards props and refs to your root component. It does not copy custom static properties, such as App.navigationOptions, onto the wrapper.
Mounting the wrapped root is what reports the app as ready: sync() calls notifyAppReady() first, which confirms the running package and arms rollback for the next one. If your app has to finish an asynchronous bootstrap (authentication, database migration) before it can vouch for the running bundle, skip wrap() and call sync() yourself at that point:
import { sync } from "@codemagic/react-native-patch";
import { bootstrapApp } from "./bootstrap"; // Your app's initialization routine.
await bootstrapApp(); // If initialization fails, do not confirm the bundle.
const status = await sync();
// "update-installed" | "up-to-date" | "embedded-revert-applied"
// | "sync-in-progress" | "error"sync() never throws. It checks for an update, downloads and installs when appropriate, and reports app readiness so a bad package can roll back.
For check / download / install as separate steps, restart timing helpers (restartApp, allowRestart / disallowRestart), getRunningBundleUpdateMetadata() to read which OTA release is running, or isNextVersionReady() to learn whether an installed update is waiting for the next reload, see the SDK types.
Configuration
Codemagic Patch is configured through native resources, not a JS API — the deployment key, URLs, public key, and binary version are read natively before the SDK initializes. Provide these values per host app:
| Resource | Required | Meaning |
| --- | --- | --- |
| CodemagicPatchDeploymentKey | yes | Deployment key for this app/track |
| CodemagicPatchApiUrl | yes | API server origin (the server's SERVER_URL), e.g. https://updates.example.com. The SDK appends /v1/... |
| CodemagicPatchDownloadBaseUrl | yes | Artifact origin (the server's PUBLIC_BASE_URL), e.g. https://storage.example.com/codemagic-patch. May include a bucket/path prefix; the SDK appends manifest/artifact paths |
| CodemagicPatchPublicKey | no | PEM public key; required only when enforcing client-side signature verification |
| CodemagicPatchMaxLaunchAttempts | no | Positive integer; consecutive launches a pending update may boot without notifyAppReady() before crash rollback. Default 3; see notifyAppReady() |
The two URLs point at different systems (API server vs. object storage / CDN), which is why one usually carries a path and the other does not.
Where the values come from: the deployment key is the DEPLOYMENT_KEY column of cmpatch deployment list --app <app-name> --format table (or the deployment's page in the dashboard); the API URL and download base URL are printed by the self-host installer when the server is set up. Use separate apps — and therefore separate deployment keys — for iOS and Android.
Bare React Native
Declare the resources.
iOS —
ios/<YourApp>/Info.plist:<key>CodemagicPatchDeploymentKey</key> <string>ios-staging-deployment-key</string> <key>CodemagicPatchDownloadBaseUrl</key> <string>https://storage-updates.example.com/codemagic-patch</string> <key>CodemagicPatchApiUrl</key> <string>https://updates.example.com</string> <!-- optional, only when enforcing code signing --> <key>CodemagicPatchPublicKey</key> <string>-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----</string> <!-- optional, defaults to 3 --> <key>CodemagicPatchMaxLaunchAttempts</key> <integer>3</integer>Android —
android/app/src/main/res/values/strings.xml:<resources> <string name="CodemagicPatchDeploymentKey" translatable="false">android-staging-deployment-key</string> <string name="CodemagicPatchDownloadBaseUrl" translatable="false">https://storage-updates.example.com/codemagic-patch</string> <string name="CodemagicPatchApiUrl" translatable="false">https://updates.example.com</string> <!-- optional, only when enforcing code signing --> <string name="CodemagicPatchPublicKey" translatable="false">-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----</string> <!-- optional, defaults to 3 --> <string name="CodemagicPatchMaxLaunchAttempts" translatable="false">3</string> </resources>Wire native bundle selection before the RN bridge starts, so boot order is pending package → current package → embedded bundle.
Android — feed
CodemagicPatch.getJSBundleFile(applicationContext)into React Native inMainApplication. On RN ≤ 0.81 (ReactNativeHost), overridegetJSBundleFile():override fun getJSBundleFile(): String? = CodemagicPatch.getJSBundleFile(applicationContext)On RN 0.82+ (new-arch
reactHost), pass it asjsBundleFilePath:override val reactHost: ReactHost by lazy { getDefaultReactHost( context = applicationContext, packageList = PackageList(this).packages, jsBundleFilePath = CodemagicPatch.getJSBundleFile(applicationContext), ) }iOS — import
CodemagicPatchClientand overridebundleURL()/sourceURL(for:)inAppDelegatewith the same selection order. Keep theDEBUGbranch pointing at Metro so local development keeps working:import CodemagicPatchClient class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { override func sourceURL(for bridge: RCTBridge) -> URL? { self.bundleURL() } override func bundleURL() -> URL? { #if DEBUG RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") #else CodemagicPatch.bundleURL() ?? Bundle.main.url(forResource: "main", withExtension: "jsbundle") #endif } }On RN ≤ 0.76, where the app template still ships an Objective-C++
AppDelegate.mm, overridesourceURLForBridge:with the same selection. The native module is a Swift pod exposed to Objective-C as@objc(CodemagicPatch); its generated-Swift.his not on the host target's header search paths and@importis unavailable in ObjC++, so forward-declare the surface you call:// CodemagicPatch is a Swift pod exposed as @objc(CodemagicPatch); forward-declare it — // the implementation links in from the pod's static library. @interface CodemagicPatch : NSObject + (NSURL *_Nullable)bundleURL; @end - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; #else return [CodemagicPatch bundleURL] ?: [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif }Expected embedded bundle names are
index.android.bundle(Android) andmain.jsbundle(iOS). If the SDK cannot determine a non-blank binary version (versionName/CFBundleShortVersionString), it no-ops and falls back to the embedded bundle.Debug builds load JS from Metro, so OTA updates are not picked up there — that is expected, not a wiring problem. To see an update apply, run a release-style build.
Register the native module for your architecture — the TurboModule (New Architecture) or the bridge module/package (Old Architecture). Autolinking handles this in most apps.
Expo apps
For projects using Prebuild, use the bundled config plugin (app.plugin.js) as shown in Quick start. Native projects maintained manually use the native configuration and bundle-selection steps instead. The plugin configuration is:
{
"plugins": [
["@codemagic/react-native-patch", {
"ios": { "deploymentKey": "<key>", "downloadBaseUrl": "<url>", "apiUrl": "<url>", "publicKey": "<pem>" },
"android": { "deploymentKey": "<key>", "downloadBaseUrl": "<url>", "apiUrl": "<url>", "publicKey": "<pem>" }
}]
]
}The plugin writes the native resources and wires bundle selection automatically (Configuration steps 1–3). Configure at least one platform, and complete every block you provide — deploymentKey, downloadBaseUrl, and apiUrl are all required per block (publicKey and maxLaunchAttempts are optional). The plugin resolves expo/config-plugins from your app's own Expo SDK, so there is nothing extra to install; expo is not a runtime or peer dependency of this package.
Usage
The simplest integration is Patch.wrap(App), which renders your root component immediately and runs sync() after it mounts. sync() checks for an update, downloads it, installs it, and reports app readiness in one call. It never throws — it resolves to a status string. Call it directly when you need to pick the moment yourself, for example after an asynchronous bootstrap:
import { sync } from "@codemagic/react-native-patch";
import { bootstrapApp } from "./bootstrap"; // Your app's initialization routine.
await bootstrapApp(); // If initialization fails, do not confirm the bundle.
const status = await sync();
// "update-installed" | "up-to-date" | "embedded-revert-applied"
// | "sync-in-progress" | "error"For finer control, drive the steps yourself and call notifyAppReady() once the app has started successfully (so the update is not rolled back):
import {
checkForUpdate,
downloadUpdate,
installUpdate,
notifyAppReady,
} from "@codemagic/react-native-patch";restartApp, allowRestart / disallowRestart, and hydrate are also exported for controlling reload timing, getRunningBundleUpdateMetadata() reports the running OTA release ({ label, packageHash, releaseNotes }, or null on the embedded bundle), and isNextVersionReady() reports whether a newer package is installed and waiting for the next reload. See the type definitions for the full API surface.
Documentation
Full integration guide, update protocol, and self-hosting instructions live in the Codemagic Patch repository.
Coming from react-native-code-push? The CodePush migration guide maps the native config resources, JS APIs, and CLI commands to their Codemagic Patch equivalents.
