HotCodePush

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.

A React Native or Expo app leaves CodePush by replacing the CodePush client with @hotcodepush/react-native-code-push at its next native release. The store build that carries the new SDK is the switch, and the binaries before it keep taking CodePush updates until their users move. You keep delta updates and your signing key. What differs, side by side, is on the site's CodePush alternative page.

HotCodePush serves no CodePush-compatible endpoint, so the CodePush client cannot be pointed at a new server URL. App Center and CodePush retired on 2025-03-31.

This guide is for the React Native and Expo apps that used react-native-code-push or Bitrise's fork @bitrise/code-push-sdk (From Bitrise CodePush). The service behind it may be App Center, Bitrise CodePush, a self-hosted code-push-server or a compatible one. A Cordova app on CodePush starts at the Cordova SDK's Installation page instead.

Install the HotCodePush skills:

npx skills add hotcodepush-team/skills

Then paste the prompt into your coding agent:

Migrate this app from CodePush to HotCodePush: install the skills with `npx skills add hotcodepush-team/skills`, then run the hotcodepush-migrate skill, following https://hotcodepush.com/docs/migrate/codepush.

The agent removes the CodePush client, runs init, maps channels, the signing key and the binary ranges, and reports a checklist for the store release. It never touches App Center or your CodePush 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 init creates both, as the quickstart shows.
  • The app is a React Native app on the New Architecture, or an Expo app that runs in a development build. The minimum versions are on Supported versions.
  • You have the private key file your CodePush releases were signed with, if the app signed. Sign your bundles explains what the key does here.
  • A store release lies ahead: the swap ships in a native build, so plan an App Store and a Play Store submission.
  • Node.js is on every machine that makes a native build, since the build step runs the CLI from an Xcode phase and a Gradle task.

Steps

If you already removed the client and ran init, skip to step 3.

1. Remove the CodePush client

npm uninstall react-native-code-push

Remove the wrapper, every call and every option per the SDK calls below. Delete the deployment key, the public key and any server URL from Info.plist and strings.xml.

Put the two host files back to what React Native's template generates for your version: the Swift file that overrides bundleURL() and MainApplication.kt. The CLI recognises the template's lines and stops with the manual step on a file it does not recognise. An Expo app removes the config plugin entry its CodePush setup added to app.json, and the next prebuild regenerates the native projects.

2. Run init

npx hotcodepush init

init signs you in, picks or creates the organization and the app with its production channel, installs the package, writes hotcodepush.json and wires the build step. It asks whether to generate a signing key: answer no if you bring the CodePush key, which step 3 adds. Decline the first release as well.

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 6, after step 5 built them.

  • React Native: init installs @hotcodepush/react-native-code-push, runs pod install and adds the Xcode phase "Create HotCodePush binary" and one apply from line to android/app/build.gradle. It edits one line in each host file, and the React Native SDK's Installation page shows them for a project it could not edit.
  • Expo: init installs @hotcodepush/expo-ota-updates and adds it to the plugins of app.json, and the config plugin wires the build step at npx expo prebuild. The prebuild refuses while the app depends on expo-updates without updates.enabled set to false. The app runs in a development build or a release build, never in Expo Go. The Expo SDK's Installation page has the details.

hotcodepush.json replaces what CodePush put into Info.plist and strings.xml: the build step writes it into the app on every build, and nothing is pasted into the native projects. The React Native SDK's Configuration page lists every key the file takes.

3. Add your signing key

Skip this step if your CodePush releases were not signed. HotCodePush signs with the same kind of RSA key pair, so the pair you have carries over:

npx hotcodepush signing-key add --private-key-path codepush-private.pem

signing-key add reads the PEM on your machine, registers the public key with the app and appends it to publicKeys in hotcodepush.json. 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. The build step writes publicKeys into the binary, which is what CodePushPublicKey did by hand.

4. Create the channels your deployments had

production exists. Each further deployment becomes a channel:

npx hotcodepush channel create --name staging --discoverable

--discoverable lets the app switch to the channel by name with setChannel({ name }), as Channels describes. Without it the channel is reachable by its id alone. A build flavour that follows staging sets HOTCODEPUSH_CHANNEL=staging in its build environment, as Set up environments and tenants shows.

5. Ship the store release that carries the SDK

Build the release as before: an Xcode archive, a Gradle release variant or a cloud build such as EAS Build. The build step runs binary create inside the build and registers the build with HotCodePush, so the machine needs npx hotcodepush login or HOTCODEPUSH_TOKEN in its environment. Under CI a store build without a token fails. npx hotcodepush binary list shows the registered builds, and you submit them to the stores.

Metro serves a debug build's JavaScript, so a debug build takes no updates: test the next step in a release build.

6. Release from your pipeline

npx hotcodepush release create --bundle-version "$(git rev-parse --short HEAD)" --yes

With HOTCODEPUSH_TOKEN and, for a signing app, HOTCODEPUSH_SIGNING_KEY in the environment, this is the whole pipeline step. The upload runs react-native bundle or expo export:embed per platform itself. Ship every commit is the pipeline this line sits in.

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.

7. Run both until the fleet moves

A binary carries one client: a CodePush release never reaches a HotCodePush build, nor the reverse. Keep the CodePush pipeline releasing while the old builds have users. The Binaries page and binary list show each new build's devices. Stop the old pipeline once the old builds are below the share you will leave behind, and delete the CodePush server or account last.

CLI commands

CodePushHotCodePush
appcenter codepush release-react -a <owner>/<app> -d Productionhotcodepush release create. The upload runs react-native bundle or expo export:embed itself, per platform.
release-react -t "1.2.x"release create --binary-version "1.2.x". Without the flag the fingerprint alone decides.
release-react -mrelease create --mandatory
release-react -r 25release create --rollout-percent 25
release-react --des "Fixes the cart"release create --notes "Fixes the cart"
release-react -k private.pemrelease create --private-key-path private.pem, or the key's text in HOTCODEPUSH_SIGNING_KEY
release-react -xrelease create --rollout-percent 0, then release rollout when ready
a disabled releaseA paused release is the counterpart: release pause keeps it for later, release revoke ends it.
appcenter codepush promote -a <owner>/<app> Staging Productionrelease create --from-channel staging --channel production
appcenter codepush rollback -a <owner>/<app> Productionrelease rollback --channel production
rollback --targetRelease v12release rollback --channel production --to-release 12
appcenter codepush patch -a <owner>/<app> Production -m truerelease update --channel production --release 43 --mandatory true
patch --des "..."release update --channel production --release 43 --notes "..."
patch -r 50release rollout --channel production --release 43 --rollout-percent 50
appcenter codepush deployment add -a <owner>/<app> Stagingchannel create --name staging
deployment ls, deployment historychannel list, release list --channel production
login --accessKey <key>HOTCODEPUSH_TOKEN in the environment, or hotcodepush login
--noDuplicateReleaseErrorNothing to pass. A rerun of release create creates the release again and fails nothing.

A release is numbered per channel, #43 of production, and the label --bundle-version gives a bundle is free text beside it. The CLI reference lists every command. release and its flags are on the release page.

SDK calls

CodePushHotCodePushNote
codePush(App), codePush(options)(App)nothingUnder checkStrategy: auto the SDK syncs on start, on resume and in the foreground, and the options move to hotcodepush.json.
codePush.sync(options, statusCallback, progressCallback)HotCodePush.sync(options?)It resolves with a typed result instead of calling back with a SyncStatus.
checkForUpdate()checkForUpdate()It answers available or up_to_date. Nothing is downloaded.
remotePackage.download(), localPackage.install(installMode)downloadUpdate({ applyStrategy }), applyUpdate()The apply moment is the strategy's.
notifyAppReady()nothing by default, or notifyReady() under readySignal: manualThe first render confirms the release.
restartApp(onlyIfUpdateIsPending)applyUpdate()It reloads only when an update waits, else answers nothing_to_apply.
disallowRestart(), allowRestart()setRestartAllowed({ isAllowed: false }), setRestartAllowed({ isAllowed: true })
clearUpdates()clearUpdates()It reloads onto the embedded bundle itself.
getUpdateMetadata(), getCurrentPackage()getState().currentReleaseIt is null on the embedded bundle.
getConfiguration()getChannel(), getDevice()
isPending, failedInstall, isFirstRun on a packagegetState().nextRelease, getState().failedBundleIds, notifyReady().previousRelease
sync({ deploymentKey })setChannel({ name }) or setChannel({ id })The choice is persisted, and setChannel(null) returns to the configured channel.
the status and progress callbacksthe sync() result, the updateAvailable, updateDownloaded, updateFailed, updateRolledBack and downloadProgress events, and useUpdates()progress runs from 0 to 1.
handleBinaryVersionMismatchCallbacksync() resolves skipped with the reason device_incompatible
updateDialognoneBuild it on updateAvailable under downloadStrategy: manual, then call downloadUpdate().
InstallMode.IMMEDIATE, ON_NEXT_RESTART, ON_NEXT_RESUMEapplyStrategy: immediate, next_start, next_resumenext_start is the default in both. ON_NEXT_SUSPEND has no equivalent, and next_resume is the closest.
minimumBackgroundDurationnextResumeMinimumBackgroundSecondsIt defaults to 300 here and to 0 there.
mandatoryInstallModemandatoryApplyStrategyIt is immediate by default, or manual.
CheckFrequency.ON_APP_START, ON_APP_RESUME, MANUALcheckStrategy: auto or manualauto checks on start, on resume and every checkIntervalSeconds, 900 by default.
ignoreFailedUpdates, rollbackRetryOptionsnoneA bundle that failed on a device is never retried there, and the next release is taken.
SyncStatusstatus on the result: up_to_date, available, downloaded, applied, skipped, failedskipped and failed carry a reason.

The React Native SDK's API page has every method, event and type. Choose an apply strategy compares the strategies the InstallMode values map to.

Before and after

The codePush() wrapper goes, and the component is exported plain:

App.tsx
import codePush from "react-native-code-push"; 

function App() {
  return <Root />;
}

export default codePush({ 
  checkFrequency: codePush.CheckFrequency.ON_APP_RESUME, 
  installMode: codePush.InstallMode.ON_NEXT_RESUME, 
  minimumBackgroundDuration: 60, 
})(App); 
export default App; 

The options the wrapper took move into hotcodepush.json:

hotcodepush.json
{
  "appId": "<your app id>",
  "channel": "production",
  "applyStrategy": "next_resume",
  "nextResumeMinimumBackgroundSeconds": 60
}

notifyAppReady() goes: the first render confirms the release. An app that confirms a release by hand sets readySignal: manual and calls notifyReady(), which also says whether this start follows a rollback. The call and its effect leave the entry file:

App.tsx
import { useEffect } from "react"; 
import codePush from "react-native-code-push"; 

export function App() {
  useEffect(() => { 
    codePush.notifyAppReady(); 
  }, []); 
  return <Root />;
}

You keep delta updates

HotCodePush precomputes a delta per base, the previous bundle and each embedded bundle, and a device without a matching pack takes the full one. CodePush diffed file by file, so a changed Hermes bundle travelled whole. Here it travels as a byte-level patch, so a small change moves little.

The store build uploads its embedded bundle, so the first update after a native release is a delta. See How an update reaches a device.

What else differs

  • Addresses: a CodePush deployment key was both the address and a bearer token in the binary. Here appId is public, and a channel is addressed by name in the configuration and by an unguessable id at runtime. Everything in a channel is readable by every device on it.
  • The fingerprint replaces the target binary version: it rides on every release, so a release never lands on a build whose native code differs. A --binary-version range over the store version narrows it. See Compatibility.
  • The readiness gate has no off switch: a release that has not rendered within readyTimeoutSeconds, 10 by default, is rolled back and marked failed there. It is never tried there again, and the next start fires updateRolledBack.
  • Rollback is a release, and rollouts are sticky: release rollback publishes the previous bundle as a new release, mandatory by default, and release revoke ends a release outright. A device keeps its release when the percentage falls, and auto-pause halts a release whose failure rate climbs. See Roll out gradually.
  • No dialog and no Windows: there is no updateDialog, and a mandatory release applies at once under mandatoryApplyStrategy: immediate. The SDK ships for iOS and Android.

From Bitrise CodePush

Bitrise's @bitrise/code-push-sdk is a fork of the CodePush client with the same JavaScript API, native module and native keys. The steps and the SDK calls above apply, and four things differ:

  • The package: npm uninstall @bitrise/code-push-sdk, and in an Expo app remove the @bitrise/code-push-sdk/expo entry from plugins. An app that kept react-native-code-push against Bitrise's server follows step 1 as written.
  • One more key: delete CodePushEnableDeltaUpdates from Info.plist and the <bool name="CodePushEnableDeltaUpdates"> line from strings.xml. It is a <bool>, so a search for <string name="CodePush misses it, and deltas need no switch here.
  • An app per platform: deployments belong to Bitrise's per-platform Release Management apps, so an iOS Staging and an Android Staging become one staging channel.
  • The pipeline: in Bitrise CI, one Script Step that runs release create replaces both codepush-publish@1 Steps, and a HOTCODEPUSH_TOKEN Secret replaces BITRISE_API_TOKEN. The store build Workflow needs the token too, and the Bitrise recipe has the full Workflow.

The CLI, bitrise :codepush as a plugin or codepush standalone, replaces appcenter codepush with its own flags:

Bitrise CodePushHotCodePush
push --bundle --platform ios --deployment Production --app-version "1.2.x"release create --channel production --binary-version "1.2.x" releases both platforms in one call, and the range is optional.
--app-version "~1.2.0", "^1.2.3", "1.2.3 - 1.2.7"--binary-version ">=1.2.0 <1.3.0", ">=1.2.3 <2.0.0", ">=1.2.3 <=1.2.7". ~, ^ and hyphen ranges do not parse.
--rollout 25, --mandatory, --disabled, --description "Fixes the cart"--rollout-percent 25, --mandatory, --rollout-percent 0, --notes "Fixes the cart"
--private-key-path private_key.pem--private-key-path private_key.pem, or the key's text in HOTCODEPUSH_SIGNING_KEY
promote --source-deployment Staging --destination-deployment Productionrelease create --from-channel staging --channel production
rollback --deployment Production --target-release v3release rollback --channel production --to-release 3
patch --deployment Production --label v5 --rollout 50release rollout --channel production --release 5 --rollout-percent 50
deployment add Beta --key <existing>channel create --name beta --discoverable. There is no key to keep.
deployment clear Staging, update remove Staging --label v3release revoke --channel staging --all, release revoke --channel staging --release 3
BITRISE_API_TOKEN, .codepush.jsonHOTCODEPUSH_TOKEN, hotcodepush.json

Next steps

  • Ship every commit has the pipeline that releases every merge with the one CLI line, named by its commit.
  • 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 InstallMode used to make.
  • React Native SDK reference documents every hotcodepush.json key, method, event, type and reason of the SDK you installed.

Last updated on

On this page