Migrate from Codemagic Patch
Move a React Native, Expo or Capacitor app from self-hosted Codemagic Patch to HotCodePush, with every command, key and call mapped, and retire the server.
A Codemagic Patch app moves to HotCodePush at its next store release and stops running its Patch server, storage and CDN. The store build that carries the new SDK is the switch, and the builds before it keep reading your Patch storage until their users move. What differs, side by side, is on the site's Codemagic Patch alternative page.
Patch runs on a stack you host, or one Codemagic hosts on request. The steps below assume your own stack. HotCodePush is hosted alone: there is no self-hosted edition, and the Patch SDK cannot be pointed at it. This guide covers React Native and Expo apps on @codemagic/react-native-patch and Capacitor apps on @codemagic/capacitor-patch.
Codemagic's legacy CodePush service runs @code-push-next/react-native-code-push, a CodePush client, which the CodePush guide covers.
AI-assisted migration (recommended)
Install the HotCodePush skills:
npx skills add hotcodepush-team/skillsThen paste the prompt into your coding agent:
Migrate this app from Codemagic Patch to HotCodePush: install the skills with `npx skills add hotcodepush-team/skills`, then run the hotcodepush-migrate skill, following https://hotcodepush.com/docs/migrate/codemagic-patch.The agent removes the Patch SDK, runs init, maps deployments, the signing key and the target versions, and reports a checklist for the store release. It never touches your Patch server, and the first release waits for the store build. Agent skills explains what the agent does.
Prerequisites
- You have a HotCodePush account and an organization.
npx hotcodepush initcreates both, as the quickstart shows. - The app is a React Native app on the New Architecture, an Expo app in a development build, or a Capacitor app on a supported major. The minimum versions are on Supported versions. Patch runs on older React Native versions, so upgrade first.
- You have the private key your Patch releases were signed with, if the app required code signing. Sign your bundles explains what the key does here.
- A store release lies ahead, and Node.js is on every build machine, since the build step runs the CLI from an Xcode phase and a Gradle task.
Steps
If you already removed Patch and ran init, skip to step 4.
1. Freeze a branch for the old builds
git branch codemagic-patch-fleetA bundle built from the migrated code lacks Patch's JavaScript, and the old binaries lack the HotCodePush native module. Hotfixes for them come from this branch, with cmpatch or cmpatch-capacitor as before.
2. Remove the Patch SDK
npm uninstall @codemagic/react-native-patchExport the root component plain instead of Patch.wrap(App, options) and replace every call per the SDK calls below. Delete codemagic-patch.config.json and the codemagicPatch key in package.json, then clean the native projects:
- the
CodemagicPatch*keys inInfo.plistandstrings.xml, and the$(PATCH_IOS_DEPLOYMENT_KEY)build setting or.xcconfigline. - the
resValueblock inandroid/app/build.gradlethat readsPATCH_ANDROID_DEPLOYMENT_KEY: delete it before the CI variable goes, since the block throws without the variable. import io.codemagic.patch.CodemagicPatchand thejsBundleFilePathline, or thegetJSBundleFile()override, inMainApplication.kt, and the Swift host file as the callout says, thenpod install.- in an Expo app, the
@codemagic/react-native-patchentry inpluginsand thePATCH_*_DEPLOYMENT_KEYvariables ineas.json. The next prebuild regenerates the native projects.
Put the Swift host file back before you run init
Patch wraps the template's call: CodemagicPatch.bundleURL() ?? Bundle.main.url(forResource: "main", withExtension: "jsbundle"). init swaps the inner call for HotCodePush.bundleURL() and leaves the rest, so the file stops compiling once the Patch pod is gone. Restore the template's line and drop import CodemagicPatchClient first, in AppDelegate.swift or whichever Swift file overrides bundleURL().
A Capacitor app runs npm uninstall @codemagic/capacitor-patch and deletes the CodemagicPatch block from plugins in capacitor.config.ts, with any CodemagicPatch* override in Info.plist and strings.xml. It replaces start() and every other call per the SDK calls below, drops npx codemagic-patch-check-config from the native build job and runs npx cap sync.
3. Run init
npx hotcodepush initinit signs you in, creates the organization and one app for both platforms with its production channel, installs the package and wires the build step. Answer no to the signing key if you bring yours, and decline the first release.
A React Native app gets @hotcodepush/react-native-code-push, with an import and the HotCodePush.bundleURL() call in the Swift file and a swapped import in MainApplication.kt. An Expo app gets @hotcodepush/expo-ota-updates in plugins, a Capacitor app @hotcodepush/capacitor-live-updates. The React Native, Expo and Capacitor installation pages show the manual steps for a project init could not edit.
Release after the store build, not before
A release created before a binary was built is never served to that binary. The release that reaches your new store builds is the one you create in step 7, after step 6 built them.
A Capacitor app then imports the package in its entry file, where start() was, even when no call follows:
import '@hotcodepush/capacitor-live-updates';The import reports the first render, which is what start() did through notifyAppReady(). An app that imports the package late, or not at all, confirms no update, and every one is rolled back after readyTimeoutSeconds. A React Native app needs no call: the first render confirms the release.
hotcodepush.json replaces the deployment key and the two URLs. The build step writes it into the app on every build, and nothing is pasted into the native projects.
4. Add your signing key
Skip this step if your Patch app did not require code signing. HotCodePush signs with the same kind of RSA key pair as openssl genrsa made for Patch, so the pair carries over:
npx hotcodepush signing-key add --private-key-path patch-private-key.pemsigning-key add reads the PEM on your machine, registers the public key with the app and appends it to publicKeys in hotcodepush.json, which replaces CodemagicPatchPublicKey. The private key stays where it is: every upload names it with --private-key-path, or CI sets the file's content as HOTCODEPUSH_SIGNING_KEY.
5. Create the channels your deployments had
production exists and takes the place of Production on both platform apps. Each further deployment becomes one channel for both platforms:
npx hotcodepush channel create --name staging --discoverable--discoverable lets the app switch to the channel by name with setChannel({ name }), as Channels describes. Patch had no runtime switch, so leave the flag out unless you add one. A build that carried the Staging key sets HOTCODEPUSH_CHANNEL=staging in its build environment instead of the PATCH_*_DEPLOYMENT_KEY variables, as Set up environments and tenants shows.
6. Ship the store release that carries the SDK
Build the release as before. The build step runs binary create inside the build and registers the build with its fingerprint, replacing the cmpatch release-react --disabled baseline per store build. The machine needs npx hotcodepush login or HOTCODEPUSH_TOKEN. Under CI a store build without a token fails, and npx hotcodepush binary list shows the registered builds.
Metro serves a React Native debug build's JavaScript, so it takes no updates: test the next step in a release build.
7. Release from your pipeline
npx hotcodepush release create --bundle-version "$(git rev-parse --short HEAD)" --yesThis line replaces both cmpatch release-react --platform runs. A Capacitor app puts npm run build && in front, in place of cmpatch-capacitor release create --bundle-path www per platform app. The upload reads webDir from capacitor.config.
HOTCODEPUSH_TOKEN replaces CODEMAGIC_PATCH_TOKEN and CODEMAGIC_PATCH_SERVER_URL, and HOTCODEPUSH_SIGNING_KEY the key file in a signing app. In an Expo app, the upload bundles in the job's shell, so give the job the EXPO_PUBLIC_* values the store build had. Ship every commit is the pipeline this line sits in, and the Codemagic recipe shows it in codemagic.yaml.
Open the app on a device that runs the new store build: the release applies within about a minute (How an update reaches a device). If nothing happens, see Troubleshooting.
8. Run both until the fleet moves
The old builds keep reading manifests from your Patch storage or CDN. Keep the server, the storage and the CDN running while they have users. Their hotfixes come from the branch of step 1, with --target-binary-version on their store versions.
cmpatch deployment timeseries or the Patch dashboard shows the old share and binary list the new one. Once the old builds are below the share you leave behind, stop the Patch pipeline and take the stack down. Nothing is imported from its metrics or release history.
Never publish a bundle from the migrated code to Patch
It carries no Patch JavaScript, so notifyAppReady() never runs: an old build rolls it back after maxLaunchAttempts launches, 3 by default, and reports it failed. The branch of step 1 keeps the Patch SDK.
CLI commands
cmpatch serves React Native and Expo apps and cmpatch-capacitor Capacitor apps, each against an app per platform. One hotcodepush CLI serves every framework and one app.
| Codemagic Patch | HotCodePush |
|---|---|
npm install -g @codemagic/patch-cli, @codemagic/capacitor-patch-cli | npm install --save-dev hotcodepush |
cmpatch login, --token, CODEMAGIC_PATCH_TOKEN, CODEMAGIC_PATCH_SERVER_URL | hotcodepush login, HOTCODEPUSH_TOKEN, with no server URL |
cmpatch token create --name ci | An API token comes from the console's API tokens page. |
cmpatch init, cmpatch wire, codemagic-patch.config.json | hotcodepush init, hotcodepush.json |
app create --name MyApp-iOS, --name MyApp-Android | One app serves both platforms, and init creates it. |
deployment create, list, rename, remove | channel create, list, update, delete |
deployment clear | release revoke --all revokes every release, and devices return to the embedded bundle. |
cmpatch release-react --platform ios --deployment Staging --yes | release create --channel staging --yes releases both platforms in one call. |
cmpatch-capacitor release create --bundle-path www --app MyApp-iOS --deployment Staging --target-binary-version 1.2.3 | release create --channel staging releases, and the upload reads webDir. |
--target-binary-version 1.2.0 | --binary-version "1.2.0" pins one store version. Without it the fingerprint alone decides. |
--rollout-percentage 25, --mandatory, --release-notes "Fixes the cart" | --rollout-percent 25, --mandatory, --notes "Fixes the cart" |
--disabled | --rollout-percent 0, then release rollout when ready. Nothing replaces the fingerprint baseline. |
--private-key-path patch-private-key.pem | --private-key-path patch-private-key.pem, or the key's text in HOTCODEPUSH_SIGNING_KEY |
--allow-fingerprint-mismatch | Nothing. A release reaches matching binaries alone, and the upload warns when none matches. |
--dry-run, --format json, --non-interactive, --no-duplicate-release-error | --dry-run, --json, --yes. A rerun of release create fails nothing. |
cmpatch bundle, then release create with the .cmpatch file | bundle upload, then release create --bundle <n> |
release promote --source-deployment Staging --dest-deployment Production --label v4 | release create --from-channel staging --channel production |
release patch --label v5 --rollout-percentage 50 | release rollout --release 5 --rollout-percent 50. The percentage moves either way. |
release patch --mandatory, --not-mandatory, --release-notes "..." | release update --mandatory true, --mandatory false, --notes "..." |
release patch --target-binary-version | Nothing. Conditions are fixed at creation, so create a new release and revoke the old one. |
release disable, release enable | release revoke, then release create --bundle <n>. release pause keeps devices on the release. |
release rollback --label v3 | release rollback --to-release 3, mandatory by default |
release inspect --wait | Nothing to wait for. release create waits until the release is live, up to a minute. |
deployment metrics, deployment timeseries, release metrics, failures | the console, and statistics get |
doctor, fingerprint --platform ios | doctor, fingerprint |
npx codemagic-patch-check-config | doctor |
selfhost install, upgrade, backup, restore, local-eval | Nothing. There is no server to run. |
A release is numbered per channel, #4 of production, where Patch labelled it v4 per deployment. --bundle-version gives a bundle a free-text label beside it. The CLI reference lists every command. release and its flags are on the release page.
What maps to what
The native keys, the config plugin's options and the options of Patch.wrap(), start() and sync() move into hotcodepush.json:
| Codemagic Patch | HotCodePush | Note |
|---|---|---|
CodemagicPatchDeploymentKey, plugins.CodemagicPatch.<platform>.deploymentKey, the plugin's deploymentKey | appId and channel in hotcodepush.json | appId is public, and the build step resolves the channel to its id. |
CodemagicPatchApiUrl, CodemagicPatchDownloadBaseUrl, apiUrl, downloadBaseUrl | none | There is no server to name. |
CodemagicPatchPublicKey, publicKey | publicKeys | signing-key add or signing-key create fills it. |
CodemagicPatchMaxLaunchAttempts, maxLaunchAttempts, 3 launches | readyTimeoutSeconds, 10 seconds | It is a window per start instead of a count of launches, with no off switch. |
$(PATCH_IOS_DEPLOYMENT_KEY), the resValue block, PATCH_*_DEPLOYMENT_KEY in eas.json or capacitor.config.ts | HOTCODEPUSH_CHANNEL in the build's environment | |
import CodemagicPatchClient, CodemagicPatch.bundleURL() ?? Bundle.main.url(...) | HotCodePush.bundleURL() | init writes it into the template's line. |
import io.codemagic.patch.CodemagicPatch, jsBundleFilePath = CodemagicPatch.getJSBundleFile(applicationContext) or the getJSBundleFile() override | the HotCodePushReactHost.getDefaultReactHost import | init swaps the import, and the call stays. |
the @codemagic/react-native-patch entry in plugins, with its ios and android blocks | @hotcodepush/expo-ota-updates in plugins, without options | |
checkFrequency: ON_APP_START, the default, and ON_APP_RESUME | checkStrategy: auto | It checks at start, on resume and every checkIntervalSeconds, 900 by default. |
installMode: ON_NEXT_RESTART, the default, ON_NEXT_RESUME, IMMEDIATE | applyStrategy: next_start, the default, next_resume, immediate | |
installMode: ON_NEXT_SUSPEND | none | next_resume is the closest. |
minimumBackgroundDuration: 60_000, milliseconds, 0 by default | nextResumeMinimumBackgroundSeconds: 60, seconds, 300 by default | Divide by 1,000, and set 0 to keep Patch's default. |
mandatoryInstallMode, IMMEDIATE by default | mandatoryApplyStrategy: immediate, the default, or manual |
The React Native SDK's Configuration page lists every key with its type and default. The Capacitor SDK's Configuration page is the same list.
SDK calls
| Codemagic Patch | HotCodePush | Note |
|---|---|---|
export default Patch.wrap(App, options) | export default App | The options move to hotcodepush.json. |
start(options) after the first screen | the import in the entry file | notifyReady() under readySignal: manual replaces a late start. |
sync(options, onProgress) | sync(options?) and the downloadProgress event | It resolves with a typed result. |
"up-to-date", "update-installed", "error" | up_to_date, then downloaded or applied, then failed with a reason | |
"sync-in-progress" | none | A second call joins the running cycle. |
checkForUpdate(), action: "ota-update" or "up-to-date" | checkForUpdate(): available or up_to_date | Nothing is downloaded. |
action: "embedded-revert", "embedded-revert-applied" | None. After release revoke the device steps back to an older release or the embedded bundle. | It has no status of its own. |
isStoreUpdateAvailable, latestBinaryVersion | none | sync() resolves skipped with device_incompatible when the newest release's conditions do not fit the binary. |
downloadUpdate(remotePackage, onProgress) | downloadUpdate({ applyStrategy }) and downloadProgress | progress runs from 0 to 1. |
installUpdate(local, { installMode }), restartApp(true) | applyUpdate() | It reloads only when an update waits, else answers nothing_to_apply. |
notifyAppReady() | nothing, or notifyReady() under readySignal: manual | The first render confirms the release. |
disallowRestart(), allowRestart() | setRestartAllowed({ isAllowed: false }), setRestartAllowed({ isAllowed: true }) | |
getRunningBundleUpdateMetadata() | getState().currentRelease | It is null on the embedded bundle in both. |
isNextVersionReady() | getState().nextRelease | |
remotePackage.previouslyFailed | getState().failedBundleIds | A failed bundle is never retried on that device in both. |
hydrate() | nothing | |
CodemagicPatchError with code | a result with status and reason | A method rejects for a programming mistake or an unconfigured build. |
The React Native SDK's API page and the Capacitor SDK's API page have every method, event and type. On the web the Capacitor package rejects with Capacitor's UNIMPLEMENTED error, where Patch threw UNSUPPORTED_ON_WEB, so guard the calls with Capacitor.isNativePlatform().
Before and after
The Patch.wrap() export goes, and the component is exported plain:
import * as Patch from '@codemagic/react-native-patch';
import { Root } from './Root';
function App() {
return <Root />;
}
export default Patch.wrap(App, {
checkFrequency: Patch.CheckFrequency.ON_APP_RESUME,
installMode: Patch.InstallMode.ON_NEXT_RESUME,
minimumBackgroundDuration: 60_000,
});
export default App; The options move into hotcodepush.json, the duration in seconds:
{
"appId": "<your app id>",
"channel": "production",
"applyStrategy": "next_resume",
"nextResumeMinimumBackgroundSeconds": 60
}In a Capacitor app start() goes, and the import takes its place in the entry file:
import { CheckFrequency, InstallMode, start } from '@codemagic/capacitor-patch';
import '@hotcodepush/capacitor-live-updates';
start({
checkFrequency: CheckFrequency.ON_APP_RESUME,
installMode: InstallMode.ON_NEXT_RESTART,
}); What differs
- Hosted, not self-hosted: no server, storage or CDN to run. Privacy and device data says where data is stored.
- The fingerprint replaces the exact target version: a release reaches every store version that shares the fingerprint. Patch served the target version and, on React Native, the versions whose recorded fingerprint matched. Pass
--binary-version "1.2.3"to keep the old behaviour. A Capacitor app gets the fingerprint too, see Compatibility. - Readiness is a window, not a launch count: a release that has not rendered within
readyTimeoutSeconds, 10 by default, is rolled back. It is never tried there again. An app that calledsync()late on purpose setsreadySignal: manualand callsnotifyReady(). release disableisrelease revoke, notrelease pause: Patch's disable moves devices back to the previous release. Hererelease revokedoes that, andrelease pausekeeps devices on the release. See Roll back a release.- Rollouts overlap and move either way: Patch allows one partial rollout per deployment at a time. Here releases never wait, and
release rolloutlowers a percentage too, and a device keeps its release when the percentage falls. Auto-pause halts a release whose failure rate climbs. See Roll out gradually.
Next steps
- Roll out gradually: start a release at a small percentage, let it progress and let auto-pause stop a bad one.
- Choose an apply strategy: pick when a downloaded update takes effect, the decision
installModeused to make. - React Native SDK reference documents every
hotcodepush.jsonkey, method, event, type and reason of the SDK you installed. - Capacitor SDK reference documents the same for a Capacitor app, with the entry-file import that reports readiness.
Last updated on
Migrate from Capgo
Replace @capgo/capacitor-updater with the HotCodePush Capacitor SDK at your next store release, with every channel, config key, call and event mapped.
Migrate from CodePush
Replace the retired CodePush client with @hotcodepush/react-native-code-push at your next native release and keep delta updates and your signing key.