Skip to main content

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 cmpatch CLI (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 keyValue
CodemagicPatchDeploymentKeythe deployment key from cmpatch deployment list
CodemagicPatchDownloadBaseUrlthe server's exact PUBLIC_BASE_URL (the bundled-storage default ends with /codemagic-patch)
CodemagicPatchApiUrlyour 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

note

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. The DEBUG / Metro branch is left untouched.
  • Android MainApplication → prefers CodemagicPatch.getJSBundleFile(applicationContext) (both the RN ≤ 0.81 getJSBundleFile() and RN ≥ 0.82 jsBundleFilePath host 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:

ProfilePATCH_IOS_DEPLOYMENT_KEYPATCH_ANDROID_DEPLOYMENT_KEY
previewiOS Staging keyAndroid Staging key
productioniOS Production keyAndroid 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.