Native setup
Native setup
Wire @codemagic/react-native-patch into a React Native or Expo app so release builds can check, download, and apply OTAs. Choose manual setup for native projects you maintain directly, or the config plugin for projects generated with Expo Prebuild.
You need:
- A running Patch server (local quickstart or Install)
- The
cmpatchCLI (npm install -g @codemagic/patch-cli) - A native project you can rebuild (Expo Go is not supported)
Create apps and deployments
From your project root, let the CLI do it:
cmpatch init
It connects to the server (or installs one), signs you in, creates or selects
one app per platform — each with Staging and Production deployments — and
writes codemagic-patch.config.json so later commands can omit --server-url
and --app. cmpatch context shows where each value comes from.
To manage apps by hand instead, keep iOS and Android in separate apps:
cmpatch app create --name MyApp-iOS
cmpatch app create --name MyApp-Android
Either way, deployment list shows the keys your app embeds (CodemagicPatchDeploymentKey):
cmpatch deployment list --app MyApp-iOS --format table
cmpatch deployment list --app MyApp-Android --format table
The same operations are available in the web dashboard: open your app, open a deployment, and copy the deployment key plus SDK URLs from the SDK configuration panel.
Install the SDK
cmpatch init wires the SDK once the project is linked, and cmpatch wire
does the same for an already linked project or a setup that stopped halfway:
cmpatch wire
It installs the package with your package manager, writes each platform's
deployment key and URLs (into Info.plist and strings.xml, or the config
plugin entry for a project that runs expo prebuild), hooks native bundle
selection, and exports the root component through Patch.wrap. The plan is
shown before anything changes; whatever it cannot do safely — a dynamic Expo
config, a second app target, a root wrapped by another library, another OTA
system still active — it lists with the exact edit for you to make. The rest
of this page is that manual path.
To do it by hand, add the package:
yarn add @codemagic/react-native-patch
The SDK is configured through native values injected at build time:
| App config key | Value |
|---|---|
CodemagicPatchDeploymentKey | the deployment key from cmpatch deployment list |
CodemagicPatchDownloadBaseUrl | the server's exact PUBLIC_BASE_URL (the bundled-storage default ends with /codemagic-patch) |
CodemagicPatchApiUrl | your API URL |
CodemagicPatchPublicKey | (optional) PEM public key for code-signing enforcement |
CodemagicPatchMaxLaunchAttempts | (optional) launches a pending update gets to call notifyAppReady() before rollback; default 3, see SDK reference |
The snippets below use placeholder values. Substitute your deployment keys from above and your API / download URLs from Install (or local quickstart for localhost).
Option A. Manual native setup
Wire the config and native bundle selection manually. This path also applies to Expo apps whose native projects are maintained directly.
For Expo apps, keep your existing AppDelegate superclass, factory, and Metro configuration. Apply only the bundle-selection changes described below within that structure.
iOS
Install the native pod:
cd ios && pod install && cd ..
Add CodemagicPatchDeploymentKey, CodemagicPatchDownloadBaseUrl, and CodemagicPatchApiUrl to 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>
In your AppDelegate, override the bundle URL so the app prefers the OTA bundle and falls back to the embedded bundle. Keep the DEBUG branch pointing at Metro so local development keeps working.
Expo (Swift AppDelegate): apply this diff inside your existing AppDelegate.swift. Keep the Expo delegate and debug bundle root:
import Expo
+import CodemagicPatchClient
// ...existing imports and AppDelegate...
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
// ...existing methods...
override func bundleURL() -> URL? {
#if DEBUG
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
#else
- Bundle.main.url(forResource: "main", withExtension: "jsbundle")
+ CodemagicPatch.bundleURL() ?? Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}
For an older Expo AppDelegate.mm, add the Objective-C++ forward declaration below and change only the embedded-bundle expression in your existing release branch. Preserve Expo's superclass and .expo/.virtual-metro-entry debug root. The following full examples are for bare React Native.
On the Swift AppDelegate (RN 0.77+ template):
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, override sourceURLForBridge: with the same selection. Forward-declare the Swift surface (the generated -Swift.h is not on the host target's search paths):
@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
}
Reference: client/plugin/src/withIosBundleURL.ts
Android
Add the same keys to 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>
In MainApplication.kt, feed the SDK's bundle path into React Native.
Expo: use the diff matching your existing host. Keep its other arguments, overrides, and Expo initialization.
For ExpoReactHostFactory (Expo SDK 55+), add the bundle path to the existing call:
import expo.modules.ExpoReactHostFactory
+import io.codemagic.patch.CodemagicPatch
// ...
override val reactHost: ReactHost by lazy {
ExpoReactHostFactory.getDefaultReactHost(
context = applicationContext,
+ jsBundleFilePath = CodemagicPatch.getJSBundleFile(applicationContext),
packageList =
PackageList(this).packages.apply {
// ...existing packages...
}
)
}
For ReactNativeHostWrapper (Expo SDK 52–54), add the override inside the wrapped host:
import expo.modules.ReactNativeHostWrapper
+import io.codemagic.patch.CodemagicPatch
// ...
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
this,
object : DefaultReactNativeHost(this) {
+ override fun getJSBundleFile(): String? =
+ CodemagicPatch.getJSBundleFile(applicationContext)
// ...existing overrides, including the Expo JS entry point...
}
)
Bare React Native: on RN ≤ 0.81 (ReactNativeHost), override getJSBundleFile() inside the host object:
import io.codemagic.patch.CodemagicPatch
// ...
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
// ...existing overrides...
override fun getJSBundleFile(): String? =
CodemagicPatch.getJSBundleFile(applicationContext)
}
On RN ≥ 0.82 (reactHost via getDefaultReactHost), pass it as jsBundleFilePath:
import io.codemagic.patch.CodemagicPatch
// ...
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList = PackageList(this).packages,
jsBundleFilePath = CodemagicPatch.getJSBundleFile(applicationContext),
)
}
Reference: client/plugin/src/withAndroidBundleFile.ts
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 (npx react-native run-ios --mode Release / npx react-native run-android --mode release).
Option B. Expo (prebuild)
Requires Expo SDK 52+ and a prebuild / development-build workflow. Expo Go is not supported.
Use this path when app config and config plugins can regenerate your native projects. If you maintain native changes directly, use manual setup instead. Expo's workflow guidance explains that Prebuild is optional; using Expo does not require regenerating native projects.
Add the config plugin to app.json / app.config.js:
{
"expo": {
"plugins": [
[
"@codemagic/react-native-patch",
{
"ios": {
"deploymentKey": "ios-staging-deployment-key",
"downloadBaseUrl": "https://storage-updates.example.com/codemagic-patch",
"apiUrl": "https://updates.example.com"
},
"android": {
"deploymentKey": "android-staging-deployment-key",
"downloadBaseUrl": "https://storage-updates.example.com/codemagic-patch",
"apiUrl": "https://updates.example.com"
}
}
]
]
}
}
Each block also accepts the optional publicKey and maxLaunchAttempts values from the table above.
Before regenerating, commit or back up your work and make sure native customizations are represented in app config or plugins. --clean deletes the existing native directories; omitting it is not a preservation strategy for manually maintained projects. See Expo's migration guidance.
Then regenerate native projects:
npx expo prebuild
cd ios && pod install && cd ..
The plugin injects the config keys (iOS Info.plist, Android strings.xml) and wires native bundle selection for you, with the same wiring shown in Option A:
- iOS AppDelegate → prefers
CodemagicPatch.bundleURL(), falling back to the embedded bundle. TheDEBUG/ Metro branch is left untouched. - Android MainApplication → prefers
CodemagicPatch.getJSBundleFile(applicationContext)(both the RN ≤ 0.81getJSBundleFile()and RN ≥ 0.82jsBundleFilePathhost shapes are handled).
Build for different deployments
Use the same project to produce Staging and Production binaries by selecting the deployment key at build time, separately for iOS and Android. API and download URLs can stay the same when both deployments use the same server. Changing these native values requires a new binary; publishing an OTA to another deployment does not switch an installed app's key.
Generated native projects (CNG)
In app.config.js / app.config.ts, read environment variables in the existing Patch plugin options instead of using fixed keys:
[
"@codemagic/react-native-patch",
{
ios: {
deploymentKey: process.env.PATCH_IOS_DEPLOYMENT_KEY,
apiUrl: "https://updates.example.com",
downloadBaseUrl: "https://storage-updates.example.com/codemagic-patch",
},
android: {
deploymentKey: process.env.PATCH_ANDROID_DEPLOYMENT_KEY,
apiUrl: "https://updates.example.com",
downloadBaseUrl: "https://storage-updates.example.com/codemagic-patch",
},
},
]
Set these variables for each EAS build profile, for example in its env field:
| Profile | PATCH_IOS_DEPLOYMENT_KEY | PATCH_ANDROID_DEPLOYMENT_KEY |
|---|---|---|
preview | iOS Staging key | Android Staging key |
production | iOS Production key | Android Production key |
The plugin writes the resolved values into native files during Prebuild. Supply the same variables when running Prebuild locally. Changing the EAS profile alone does not rewrite already-generated native files uploaded with the build; use the manual configuration below if you maintain those files directly.
EXPO_PUBLIC_* variables are a separate concern: they are inlined into the JavaScript bundle, and cmpatch release-react does not read EAS profiles, so the OTA job must set them itself. See Keep bundle environment in sync with the native build.
Manually maintained native projects
iOS: replace the deployment-key value in the app target's Info.plist with a build-setting reference:
<key>CodemagicPatchDeploymentKey</key>
<string>$(PATCH_IOS_DEPLOYMENT_KEY)</string>
Define the user-defined PATCH_IOS_DEPLOYMENT_KEY setting on the app target for each build configuration, or in its existing .xcconfig file:
PATCH_IOS_DEPLOYMENT_KEY = ios-staging-deployment-key
Use the Production key in the production configuration. Select the matching scheme/build configuration locally and with EAS's ios.buildConfiguration. If your CI supplies this value through an environment variable, explicitly connect it to the Xcode build setting, for example by generating an included .xcconfig before the native build. An EAS env entry alone is not a replacement for that configuration.
Android: remove CodemagicPatchDeploymentKey from src/main/res/values/strings.xml and generate it in android/app/build.gradle instead. Add this to the existing android.defaultConfig block (Groovy):
def patchDeploymentKey = System.getenv("PATCH_ANDROID_DEPLOYMENT_KEY")
if (!patchDeploymentKey?.trim()) {
throw new GradleException("PATCH_ANDROID_DEPLOYMENT_KEY is required")
}
resValue "string", "CodemagicPatchDeploymentKey", patchDeploymentKey
Set the variable in the EAS profile's env field, or in the environment running Gradle locally. Keep the API and download URL resources from the setup above; do not also define the generated deployment-key resource in strings.xml.
Before distributing either binary, confirm that its packaged Info.plist or Android string resource contains the intended deployment key.
Next: Checking for updates. API details: SDK reference.