Migrate from Hot Updater
Move a React Native or Expo app from self-hosted Hot Updater to HotCodePush, map every command and call, and retire the bucket and the database.
A Hot Updater app moves to HotCodePush at its next store release and stops running its bucket, database and function for a hosted bill. Channels keep their names, the fingerprint replaces both update strategies and release create replaces deploy.
This page uses Hot Updater v1's names, where HotUpdater.init() returns the hotUpdater instance. A v0 app has the static HotUpdater.wrap and HotUpdater.checkForUpdate instead, and an older one runUpdateProcess and setChannel.
What you give up and gain
You gain a hosted console with roles and protected channels, auto-pause on every release and progression with --progression, and built-in statistics. The bill replaces a licence with no fee: Free up to 1,000 monthly active devices, then Pay-as-you-go (Billing and MAU).
No counterpart here: your own data location, Remote Config, KMS signing, sub-percent rollout steps and cohorts edited after a release.
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 Hot Updater to HotCodePush: install the skills with `npx skills add hotcodepush-team/skills`, then run the hotcodepush-migrate skill, following https://hotcodepush.com/docs/migrate/hot-updater.The agent removes Hot Updater, runs init, maps channels, the signing key and the binary ranges, and reports a checklist for the store release. It never touches your bucket, database or function, 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, or an Expo app that runs in a development build. The minimum versions are on Supported versions. Hot Updater ran on older versions, so upgrade first.
- You have the private key under
keys/, if the app signed with a local key. A key in a KMS cannot be exported, andinitgenerates a new one. - 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 Hot Updater and ran init, skip to step 4.
1. Freeze a branch for the old builds
git branch hot-updater-fleetA bundle built from the migrated code imports a native module the old binaries lack. Hotfixes for them come from a branch that keeps the old client, hot-updater.config.ts and the deploy job. .env.hotupdater is untracked, so move its values into that pipeline's secrets first.
2. Remove Hot Updater
npm uninstall hot-updater @hot-updater/react-nativeUninstall every other @hot-updater/* package too. Delete hot-updater.config.ts, fingerprint.json and .env.hotupdater, remove the call to HotUpdater.init() in src/hotUpdater.ts, and replace every call per the SDK calls below. Then clean the native projects:
HotUpdater.bundleURL()in the SwiftbundleURL(), andimport com.hotupdater.HotUpdaterwith thejsBundleFilePathorgetJSBundleFile()override inMainApplication.kt: put both files back to the template, whichinitrecognises (Installation).- the
HOT_UPDATER_*keys inInfo.plistand build variables, thecom.hotupdater.*meta-data in the manifest and a v0 app'sstrings.xmlentries, thenpod install. - in an Expo app, the plugin and
@hot-updater/expo/babel-pluginin the app config and theEXPO_PUBLIC_HOT_UPDATER_*variables. The prebuild refusesexpo-updatesunlessupdates.enabledisfalse.
3. Run init
npx hotcodepush initinit signs you in, creates the app with its production channel, installs the package and wires the build step. An Expo app gets @hotcodepush/expo-ota-updates and its config plugin (Expo installation). Answer no to the signing key if you bring yours, and decline the first release.
Release after the store build, not before
A release created before a binary was built is never served to that binary. Create the release in step 7, after the store build of step 6.
4. Add the signing key
npx hotcodepush signing-key add --private-key-path keys/private-key.pemsigning-key add takes an RSA key of at least 2,048 bits and fills publicKeys in hotcodepush.json, which replaces HOT_UPDATER_PUBLIC_KEY. Skip the step if the app did not sign, or if init generated a key. The private key stays where it is, and CI holds its content as HOTCODEPUSH_SIGNING_KEY.
5. Create the channels
npx hotcodepush channel create --name staging --discoverableproduction exists, and every other channel is created once, with --discoverable where the app switches to it by name. The channel set command becomes channel in hotcodepush.json, or HOTCODEPUSH_CHANNEL in a flavour's build environment (Set up environments and tenants).
6. Ship the store release that carries the SDK
Build the release as before. The build step registers the build as a binary with its fingerprint. 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.
7. Release from your pipeline
npx hotcodepush release create --bundle-version "$(git rev-parse --short HEAD)" --yesThis line replaces both deploy -p jobs. In CI, HOTCODEPUSH_TOKEN replaces .env.hotupdater, and HOTCODEPUSH_SIGNING_KEY the PEM secret (Ship every commit).
On a device running the new build the release applies within about a minute (How an update reaches a device). If nothing happens, see Troubleshooting.
8. Run both until the old builds are gone
The old builds keep calling your Hot Updater server, so keep the function, the storage, the database and the client API key live. Nothing is imported from their history. The shift shows in binary list and in Insights.
Afterwards: the bucket and the database
Once the old builds are below the share you will leave behind:
- Stop the deploy job on the frozen branch and leave every bundle enabled.
- Delete the function, the self-hosted console, the CI secrets and the API keys.
- Export Insights if you want the history, then delete the bucket and the database last.
Before the server goes, check on an old build what it runs once nothing answers.
What maps to what
| Hot Updater | HotCodePush | Note |
|---|---|---|
build, storage, database and the provider packages in hot-updater.config.ts | nothing | HotCodePush is hosted, and release create bundles itself. |
plugins: [apiKeys(), insights(), remoteConfig()] | nothing | Statistics are built in, and Remote Config has no equivalent. |
updateStrategy: "appVersion", a range per bundle | the fingerprint on every release, plus --binary-version <range> to keep one store version | Without the range a release spans every store version sharing the fingerprint. |
updateStrategy: "fingerprint", fingerprint.json | the fingerprint, computed at upload and stamped into every build | There is nothing to commit. |
fingerprint.extraSources | extraFingerprintPaths in hotcodepush.json | |
fingerprint.nativeVersion | nothing | |
signing: { enabled: true, privateKeyPath }, keys/public-key.pem | --private-key-path or HOTCODEPUSH_SIGNING_KEY, and publicKeys in hotcodepush.json | |
.env.hotupdater, baseURL, the x-api-key client key | HOTCODEPUSH_TOKEN in CI, and the public appId in hotcodepush.json | No key ships in the binary. |
HOT_UPDATER_CHANNEL, HOT_UPDATER_PUBLIC_KEY, the com.hotupdater.* meta-data | channel and publicKeys in hotcodepush.json, or HOTCODEPUSH_CHANNEL per build | The build step writes the file into the app. |
HotUpdater.bundleURL(), HotUpdater.getJSBundleFile() | HotCodePush.bundleURL(), the HotCodePushReactHost.getDefaultReactHost import | init writes both. |
@hot-updater/expo and its babel plugin | @hotcodepush/expo-ota-updates in plugins | There is no babel plugin. |
a rollout cohort of 1 to 1,000 per install, -r, --rollout-cohort-count | a bucket of 0 to 99 per device and release, --rollout-percent, release rollout | Percentages are whole. See Rollouts and auto-pause. |
setCohort("qa"), --target-cohorts | setAttributes({ cohort: 'qa' }), --attribute cohort=qa | Conditions are fixed at creation. See Target devices with attributes. |
bundle disable <id> | release revoke ends the release for good, and devices step back to an older release or the embedded bundle. | release pause keeps a release for later. |
| a crash before the first render restores the stable bundle | the readiness gate: the first render within readyTimeoutSeconds, then auto-pause over the fleet | See Roll back a release. |
CLI commands
| Hot Updater | HotCodePush |
|---|---|
init, doctor | init, doctor |
deploy -p ios, deploy -p android | release create releases both platforms by default, or --platform ios releases one. |
deploy -t "^1.2.3" | --binary-version ">=1.2.3 <2.0.0". ^, ~ and hyphen ranges do not parse. |
deploy -c staging, -f, -r 25, -m "..." | --channel staging, --mandatory, --rollout-percent 25, --notes "..." |
deploy -d | bundle upload, then release create --bundle <n> when ready |
bundle list, bundle show | release list, release get |
bundle update --rollout-cohort-count 500, --force-update, --message | release rollout --rollout-percent 50, release update --mandatory, --notes |
bundle update --target-app-version, --target-cohorts | Nothing. Conditions are fixed at creation, so create a new release. |
bundle disable, bundle enable | release revoke is final, and release pause and release resume keep one. |
v0 rollback <channel> | release rollback publishes the previous bundle as a new release, mandatory by default. |
bundle promote <id> -t production | release create --from-channel staging --channel production |
bundle delete, storage prune | bundle delete deletes one, and a bundle no release serves for 90 days expires. |
patch | Nothing. Deltas are automatic. |
channel set | channel in hotcodepush.json, and channel create in HotCodePush |
fingerprint create, fingerprint hash | fingerprint, fingerprint diff |
keys generate, keys export-public | signing-key create, signing-key add |
console, insights | the hosted console, statistics get |
api-key, db, infra, agent, remote-config | nothing |
The CLI reference lists every command, and Compatibility has the range syntax.
SDK calls
| Hot Updater | HotCodePush | Note |
|---|---|---|
HotUpdater.init(), hotUpdater.wrap({ updateStrategy })(App) | Nothing. Export App plain. | The strategies live in hotcodepush.json. |
fallbackComponent | a component on useUpdates() | It reads isSyncing and downloadProgress. |
reloadOnForceUpdate: true, false | mandatoryApplyStrategy: immediate, manual | |
onProgress, useHotUpdaterStore() | the downloadProgress event, useUpdates() | progress runs from 0 to 1 in both. |
onUpdateProcessCompleted, onError | the sync() result, and the updateAvailable, updateDownloaded and updateFailed events | sync() resolves with a typed result. |
onNotifyAppReady | notifyReady(), updateRolledBack | isRolledBack is on the result. |
checkForUpdate({ updateStrategy }) | checkForUpdate() | It answers up_to_date instead of null. |
updateInfo.updateBundle(), reload() | downloadUpdate(), applyUpdate() | applyUpdate() reloads only when an update waits. |
checkForUpdate({ channel }), resetChannel() | setChannel({ name }) then sync(), setChannel(null) | The name needs a discoverable channel, and setChannel fetches nothing by itself. |
getChannel(), getDefaultChannel(), isChannelSwitched() | getChannel(), with source reading runtime or config | |
getBundleId(), getActiveUpdateState(), isUpdateDownloaded() | getState(): currentRelease, nextRelease | |
getCrashHistory(), clearCrashHistory() | getState().failedBundleIds, clearUpdates() | clearUpdates() also reloads the embedded bundle. |
getAppVersion(), getInstallId(), the fingerprint | getDevice() | |
setCohort("qa") | setAttributes({ cohort: 'qa' }) |
The React Native SDK's API page has every method, event and type.
Before and after
The wrap call goes: checkStrategy: auto checks at start, on resume and every 900 seconds, where wrap checked once at mount. A force update maps onto a mandatory release, which reloads at once by default:
{
"appId": "<your app id>",
"channel": "production",
"mandatoryApplyStrategy": "manual"
}With manual the app keeps the moment, as reloadOnForceUpdate: false did: updateDownloaded carries release.isMandatory, and applyUpdate() reloads (Choose an apply strategy). A launch-only check is checkStrategy: manual with one sync() at start.
What differs
Never send a bundle from the migrated code to the old builds
Under appVersion a range checks no native code, so a broad -t would hand a bundle from the migrated code to the old builds. Under fingerprint the new native package changes the hash and the deploy stops.
Disabling every bundle sends old devices back to the embedded JavaScript
When no enabled bundle is left for a build, Hot Updater runs the built-in bundle, and wrap reloads for it. Leave the old bundles enabled until the old builds are gone.
The readiness timeout has no counterpart
Hot Updater has no timer. Here a release that has not rendered within readyTimeoutSeconds, 10 by default, is rolled back and marked failed on that device, with no off switch. Raise the number for a slow first start.
The two fingerprints also hash different things: HotCodePush reads the native packages in the lockfile. The Hermes and New Architecture flags, patches/ and local codegen specs count only under extraFingerprintPaths (Compatibility).
Next steps
- Choose an apply strategy decides when a downloaded update takes effect, and who restarts the app after a mandatory release.
- Roll out gradually: start small, let the release progress and let auto-pause stop a bad one.
- React Native SDK reference documents every
hotcodepush.jsonkey, method, event and type of the SDK you installed.
Last updated on
Migrate from EAS Update
Replace expo-updates with @hotcodepush/expo-ota-updates at your next store release, with channels, the runtime version and your signing key mapped.
Migrate from Ionic Appflow
Choose Capawesome Cloud or HotCodePush for your Appflow app, then move Appflow Live Updates to the HotCodePush Capacitor SDK step by step.