Migrating from CodePush
Migrating from CodePush
This guide covers moving from any CodePush-compatible client and server (community forks, hosted successors, or a self-hosted CodePush-compatible API) to self-hosted Codemagic Patch. The wire protocol and package names differ by fork; the apps / deployments / sync() mental model usually transfers. For trade-offs, see How Patch compares.
- React Native
- Capacitor
The steps below replace a React Native CodePush-compatible SDK with @codemagic/react-native-patch.
CodePush-compatible clients support React Native only. If you use Ionic Appflow Live Updates, see Migrating from Appflow.
The Patch SDK ships in the native binary. Plan a store release (or internal distribution build) that embeds Patch before OTA updates can run through your Patch server. You cannot swap the update client purely over the air.
Concept mapping
- React Native
- Capacitor
| CodePush fork (typical) | Codemagic Patch |
|---|---|
| Hosted or self-hosted CodePush-compatible server | Self-hosted Patch stack on your domains |
| App access key / service token | OAuth sign-in (CLI/dashboard) + cm_pat_… API tokens (CI/automation) |
| Deployment key in native config | CodemagicPatchDeploymentKey (same idea) |
Staging / Production deployments | Same channel names by default |
codePush.sync() / HOC wrapper | sync() / Patch.wrap(App, options?) from @codemagic/react-native-patch |
release-react (CodePush CLI) | cmpatch release-react |
--mandatory, rollout, target binary version | Same flags on cmpatch release-react / release patch |
notifyAppReady() | Called automatically by sync(); manual if not using sync() |
Install modes (ON_NEXT_RESTART, IMMEDIATE, etc.) and mandatoryInstallMode map closely, see Applying updates.
| CodePush fork (typical) | Codemagic Patch (Capacitor) |
|---|---|
| Hosted or self-hosted CodePush-compatible server | Self-hosted Patch stack on your domains |
| App access key / service token | GitHub OAuth (dashboard) + cm_pat_… API tokens (CLI/CI) |
| Deployment key in native config | deploymentKey in capacitor.config.ts (plugins.CodemagicPatch.{ios,android}) |
Staging / Production deployments | Same channel names by default |
| HOC wrapper / automatic sync on start and resume | start({ checkFrequency }) from @codemagic/capacitor-patch |
codePush.sync() | sync() |
release-react (CodePush CLI) | cmpatch-capacitor release create --bundle-path www after your web build |
--mandatory, rollout, target binary version | Same flags on cmpatch-capacitor release create / release patch. --target-binary-version is required and must be exact |
notifyAppReady() | Called automatically by start() and sync(); manual if you use neither |
Install modes (ON_NEXT_RESTART, IMMEDIATE, ON_NEXT_RESUME, ON_NEXT_SUSPEND) and mandatoryInstallMode work the same way; see Applying updates. InstallMode and CheckFrequency are exported as constants, and string literals are also accepted.
1. Stand up Patch
Follow Install (or the Local quickstart to try the stack locally first). You need:
- React Native
- Capacitor
- API URL →
CodemagicPatchApiUrl - Download base URL →
CodemagicPatchDownloadBaseUrl
Create apps to mirror your CodePush layout (one app per platform is the usual pattern). From your project root, cmpatch init connects to the server, signs you in, creates one app per platform, and links the project:
cmpatch init
cmpatch deployment list --app MyApp-iOS --format table
- API URL →
apiUrl - Download base URL →
downloadBaseUrl
Create one app per platform with cmpatch-capacitor. To install the CLI, see Native setup.
cmpatch-capacitor login --server-url https://updates.example.com
cmpatch-capacitor app create --name MyApp-iOS --server-url https://updates.example.com
cmpatch-capacitor app create --name MyApp-Android --server-url https://updates.example.com
cmpatch-capacitor deployment list --app MyApp-iOS --server-url https://updates.example.com
Copy each deployment key into config when you integrate the SDK:
- React Native
- Capacitor
Info.plist / strings.xml, or the Expo config plugin. See Native setup.
capacitor.config.ts under plugins.CodemagicPatch.ios / .android. See Native setup.
2. Replace the client SDK
- React Native
- Capacitor
Remove your CodePush client package and native wiring (package name varies by fork):
yarn remove @code-push-next/react-native-code-push # or your fork's package
While CodePush is still referenced from the native hosts, the wiring cmpatch init runs reports it as a conflict and leaves the native and JavaScript steps for later; once it is gone, run cmpatch wire to install the SDK, run pod install, and apply the native changes described in Native setup.
Replace CodePush calls in JS (example uses @code-push-next; adjust the import for your fork):
// Before (CodePush)
import codePush from "@code-push-next/react-native-code-push";
codePush.sync({ installMode: codePush.InstallMode.ON_NEXT_RESTART });
// After (Patch)
import { InstallMode, sync } from "@codemagic/react-native-patch";
void sync({ installMode: InstallMode.ON_NEXT_RESTART });
If you used the codePush(...)(App) HOC, replace it with Patch.wrap(App, options):
- import codePush from "@code-push-next/react-native-code-push";
+ import * as Patch from "@codemagic/react-native-patch";
- export default codePush({ installMode: codePush.InstallMode.ON_NEXT_RESUME })(App);
+ export default Patch.wrap(App, { installMode: Patch.InstallMode.ON_NEXT_RESUME });
The wrapper is for basic automatic updates: it renders your root immediately and calls sync() after mount. checkFrequency accepts "ON_APP_START" (default) or "ON_APP_RESUME" (mount plus foreground return). Use Patch.CheckFrequency and Patch.InstallMode constants, or their string values; do not reuse CodePush's numeric constants or copy the old options object wholesale.
For progress UI, user confirmation, or readiness that depends on asynchronous initialization, use direct APIs instead of the wrapper. See Manual control. The wrapper reference covers its props, refs, and lifecycle.
Install @codemagic/capacitor-patch with npm install @codemagic/capacitor-patch and run npx cap sync; see Native setup. Then set deploymentKey, apiUrl, and downloadBaseUrl for each platform in capacitor.config.ts.
Replace CodePush automatic sync with a call to start() after the first screen renders:
import { CheckFrequency, InstallMode, start } from "@codemagic/capacitor-patch";
start({
checkFrequency: CheckFrequency.ON_APP_RESUME,
installMode: InstallMode.ON_NEXT_RESTART,
});
start() is the Capacitor equivalent of the React Native wrapper and accepts the same options. For progress UI or user confirmation, see Manual control.
Convert background duration to milliseconds
CodePush's minimumBackgroundDuration is in seconds; Patch's is in milliseconds. Convert the value when migrating ON_NEXT_RESUME or ON_NEXT_SUSPEND:
- minimumBackgroundDuration: 60, // CodePush: one minute
+ minimumBackgroundDuration: 60_000, // Patch: one minute
Keeping 60 would reduce the threshold to 60 milliseconds. This option controls installation timing, not how often you check for updates; see Applying updates.
- React Native
- Capacitor
Replace a MANUAL wrapper without losing readiness confirmation
The legacy codePush({ checkFrequency: codePush.CheckFrequency.MANUAL })(App) wrapper still called notifyAppReady() on mount. Patch has no MANUAL wrapper policy. Remove the wrapper and preserve that startup responsibility separately from your user-triggered checks:
import * as Patch from "@codemagic/react-native-patch";
export default App; // no update wrapper
// Call from your successful startup path, after required initialization.
async function onAppStarted() {
await Patch.notifyAppReady();
}
// Connect separately to your "Check for updates" button.
async function onCheckForUpdates() {
const status = await Patch.sync();
return status; // Use the result in your UI.
}
Do not wait for the user to press the button to confirm readiness: an unconfirmed healthy update can eventually roll back after repeated launches without confirmation. If your startup path already calls sync() after initialization succeeds, it confirms readiness itself and no separate notifyAppReady() call is needed.
Move custom behavior to direct APIs
The legacy HOC discovered methods on the wrapped class component; these were not callback fields in its options. Choose the manual integration that matches what those methods did:
| Legacy behavior | Patch migration |
|---|---|
codePushDownloadDidProgress | Use sync(options, onProgress) for download progress; see progress and status. |
codePushStatusDidChange for completion or errors | Await sync() and handle its returned status. Patch returns status strings, not CodePush's numeric SyncStatus values. |
codePushStatusDidChange for checking / downloading / installing UI | Update your UI around each direct API call; see the manual example. sync() does not expose intermediate status callbacks. |
updateDialog | Show your own confirmation UI before download or installation using manual control. |
codePushOnBinaryVersionMismatch | Use checkForUpdate() and its store-version fields; see store updates. sync() returns only a status. |
| Custom static properties on the wrapped component | wrap() does not hoist them. Keep the original component where consumers need its statics, or explicitly expose the properties your integration requires. |
Per-call deploymentKey override | Not supported by Patch's JS APIs. Configure deployment keys in the native build; see per-build configuration. Apps that switch deployments at runtime need to revise that integration. |
ignoreFailedUpdates / rollbackRetryOptions | No equivalent sync() options. Patch's sync() skips packages marked previouslyFailed; it does not provide CodePush's configurable rollback retry policy. |
Use one owner for automatic checks. When replacing the wrapper with manual lifecycle code, remove Patch.wrap() rather than adding a second sync() call to observe its work: overlapping calls return "sync-in-progress" and do not attach callbacks to the running operation.
There is no wrapper component. start() marks the running bundle as healthy when called, so call it after the first screen renders, or after asynchronous initialization completes. If you replace start() with a manual flow, call notifyAppReady() on every successful startup. Progress UI, confirmation dialogs, and store update prompts use the same manual APIs as React Native. Per-call deploymentKey overrides are not supported; set keys in capacitor.config.ts.
3. Point CI at Patch
- React Native
- Capacitor
Install and authenticate the Patch CLI:
npm install -g @codemagic/patch-cli
cmpatch token create --name ci # store cm_pat_… as a CI secret
Commit the codemagic-patch.config.json that cmpatch init wrote, and CI
needs only the token; without it, set CODEMAGIC_PATCH_SERVER_URL.
Create a CI token and store the cm_pat_… value as a secret:
cmpatch-capacitor token create --name ci --expires-in-days 365
Set CODEMAGIC_PATCH_TOKEN and CODEMAGIC_PATCH_SERVER_URL in the pipeline. There is no project config file to commit.
Swap your fork's release / release-react CI steps for:
- React Native
- Capacitor
cmpatch release-react \
--platform ios \
--deployment Production \
--release-notes "…" \
--yes
cmpatch-capacitor release create \
--bundle-path www \
--app MyApp-iOS \
--deployment Production \
--target-binary-version "1.2.0" \
--release-notes "…" \
--yes
See CI integration.
4. Cut over
- React Native
- Capacitor
- Publish a native build with the Patch SDK and deployment keys embedded (Staging first).
- Publish an OTA release to that deployment with
cmpatch release-reacttargeting the same binary version users install. - Validate on Staging, then repeat for Production.
- Decommission the old CodePush server or SaaS once no active binaries still call it.
- Publish a native build with the Patch SDK and deployment keys embedded (Staging first).
- Publish an OTA release to that deployment with
cmpatch-capacitor release create --bundle-path wwwtargeting the same binary version users install. - Validate on Staging, then repeat for Production.
- Decommission the old CodePush server or SaaS once no active binaries still call it.
Checklist
- React Native
- Capacitor
- Patch server running with HTTPS and an OAuth sign-in provider configured
- Apps and deployments created; keys copied into native config
- CodePush package and native wiring removed; Patch SDK integrated
-
Patch.wrap(),sync(), or manual APIs replace CodePush sync/HOC - Background duration converted to milliseconds; Patch constants replace CodePush constants
- Startup readiness preserved when removing a
MANUALHOC - Legacy callbacks, custom statics, and unsupported options reviewed against the migration table
- CI secrets and release commands updated to
cmpatch - New store/internal binary shipped; OTA tested on Staging before Production
- Patch server running with HTTPS and an OAuth sign-in provider configured
- One Capacitor app per platform created; keys copied into
capacitor.config.ts -
@codemagic/capacitor-patchinstalled andnpx cap syncrun -
start(),sync(), or manual APIs replace CodePush-style sync - Background duration converted to milliseconds
- Startup readiness confirmed with
start(),sync(), ornotifyAppReady() - Unsupported options (per-call
deploymentKey, rollback retry policy) reviewed - CI secrets and release commands updated to
cmpatch-capacitor, with an exact--target-binary-version - New store/internal binary shipped; OTA tested on Staging before Production