HotCodePush

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.

Install the HotCodePush skills:

npx skills add hotcodepush-team/skills

Then 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 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. 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, and init generates 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-fleet

A 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-native

Uninstall 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 Swift bundleURL(), and import com.hotupdater.HotUpdater with the jsBundleFilePath or getJSBundleFile() override in MainApplication.kt: put both files back to the template, which init recognises (Installation).
  • the HOT_UPDATER_* keys in Info.plist and build variables, the com.hotupdater.* meta-data in the manifest and a v0 app's strings.xml entries, then pod install.
  • in an Expo app, the plugin and @hot-updater/expo/babel-plugin in the app config and the EXPO_PUBLIC_HOT_UPDATER_* variables. The prebuild refuses expo-updates unless updates.enabled is false.

3. Run init

npx hotcodepush init

init 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.pem

signing-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 --discoverable

production 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)" --yes

This 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:

  1. Stop the deploy job on the frozen branch and leave every bundle enabled.
  2. Delete the function, the self-hosted console, the CI secrets and the API keys.
  3. 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 UpdaterHotCodePushNote
build, storage, database and the provider packages in hot-updater.config.tsnothingHotCodePush is hosted, and release create bundles itself.
plugins: [apiKeys(), insights(), remoteConfig()]nothingStatistics are built in, and Remote Config has no equivalent.
updateStrategy: "appVersion", a range per bundlethe fingerprint on every release, plus --binary-version <range> to keep one store versionWithout the range a release spans every store version sharing the fingerprint.
updateStrategy: "fingerprint", fingerprint.jsonthe fingerprint, computed at upload and stamped into every buildThere is nothing to commit.
fingerprint.extraSourcesextraFingerprintPaths in hotcodepush.json
fingerprint.nativeVersionnothing
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 keyHOTCODEPUSH_TOKEN in CI, and the public appId in hotcodepush.jsonNo key ships in the binary.
HOT_UPDATER_CHANNEL, HOT_UPDATER_PUBLIC_KEY, the com.hotupdater.* meta-datachannel and publicKeys in hotcodepush.json, or HOTCODEPUSH_CHANNEL per buildThe build step writes the file into the app.
HotUpdater.bundleURL(), HotUpdater.getJSBundleFile()HotCodePush.bundleURL(), the HotCodePushReactHost.getDefaultReactHost importinit writes both.
@hot-updater/expo and its babel plugin@hotcodepush/expo-ota-updates in pluginsThere is no babel plugin.
a rollout cohort of 1 to 1,000 per install, -r, --rollout-cohort-counta bucket of 0 to 99 per device and release, --rollout-percent, release rolloutPercentages are whole. See Rollouts and auto-pause.
setCohort("qa"), --target-cohortssetAttributes({ cohort: 'qa' }), --attribute cohort=qaConditions 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 bundlethe readiness gate: the first render within readyTimeoutSeconds, then auto-pause over the fleetSee Roll back a release.

CLI commands

Hot UpdaterHotCodePush
init, doctorinit, doctor
deploy -p ios, deploy -p androidrelease 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 -dbundle upload, then release create --bundle <n> when ready
bundle list, bundle showrelease list, release get
bundle update --rollout-cohort-count 500, --force-update, --messagerelease rollout --rollout-percent 50, release update --mandatory, --notes
bundle update --target-app-version, --target-cohortsNothing. Conditions are fixed at creation, so create a new release.
bundle disable, bundle enablerelease 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 productionrelease create --from-channel staging --channel production
bundle delete, storage prunebundle delete deletes one, and a bundle no release serves for 90 days expires.
patchNothing. Deltas are automatic.
channel setchannel in hotcodepush.json, and channel create in HotCodePush
fingerprint create, fingerprint hashfingerprint, fingerprint diff
keys generate, keys export-publicsigning-key create, signing-key add
console, insightsthe hosted console, statistics get
api-key, db, infra, agent, remote-confignothing

The CLI reference lists every command, and Compatibility has the range syntax.

SDK calls

Hot UpdaterHotCodePushNote
HotUpdater.init(), hotUpdater.wrap({ updateStrategy })(App)Nothing. Export App plain.The strategies live in hotcodepush.json.
fallbackComponenta component on useUpdates()It reads isSyncing and downloadProgress.
reloadOnForceUpdate: true, falsemandatoryApplyStrategy: immediate, manual
onProgress, useHotUpdaterStore()the downloadProgress event, useUpdates()progress runs from 0 to 1 in both.
onUpdateProcessCompleted, onErrorthe sync() result, and the updateAvailable, updateDownloaded and updateFailed eventssync() resolves with a typed result.
onNotifyAppReadynotifyReady(), updateRolledBackisRolledBack 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 fingerprintgetDevice()
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:

hotcodepush.json
{
  "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

Last updated on

On this page