HotCodePush

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.

An Expo app leaves EAS Update by replacing expo-updates with @hotcodepush/expo-ota-updates at its next store release. The store build that carries the new SDK is the switch, and the binaries before it keep taking EAS updates until their users move. What differs, side by side, is on the site's EAS Update alternative page.

HotCodePush serves no EAS-compatible endpoint, so expo-updates cannot be pointed at it. The minimum versions are on Supported versions. An app on a newer Expo SDK than the SDK supports waits until that page lists it.

A bare React Native app on expo-updates starts at the React Native 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 EAS Update to HotCodePush: install the skills with `npx skills add hotcodepush-team/skills`, then run the hotcodepush-migrate skill, following https://hotcodepush.com/docs/migrate/eas-update.

The agent removes expo-updates, runs init, maps channels, the signing key, config keys and SDK calls, and reports a checklist for the store release. It never touches EAS Update, 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 an Expo app that runs in a development build or a release build, never Expo Go.
  • You have the private key file your EAS updates were signed with, if code signing was on. 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 expo-updates and ran init, skip to step 4.

1. Freeze a branch for the old builds

git branch eas-update-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.

2. Remove expo-updates

npm uninstall expo-updates

Remove the updates block and runtimeVersion from the app config, and every expo-updates import per the SDK calls below. Keep extra.eas.projectId while EAS Build makes your binaries. Remove channel from each build profile in eas.json, since step 5 replaces it.

The prebuild refuses a project that lists expo-updates unless updates.enabled is false.

3. Run init

npx hotcodepush init

init signs you in, picks or creates the organization and the app with its production channel, installs @hotcodepush/expo-ota-updates and writes hotcodepush.json. It adds the package to plugins in app.json or app.config.json. A project whose app config is code adds the entry itself.

It asks whether to generate a signing key: answer no if you bring the EAS key, which step 4 adds. Decline the first release too. Pin the CLI with npm install --save-dev hotcodepush: every native build runs it.

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.

The config plugin wires the build step at npx expo prebuild. hotcodepush.json takes over from updates.url, runtimeVersion and the expo-channel-name header: appId and channel are the whole file. The Expo SDK's Configuration page lists every other key.

4. Add your signing key

Skip this step if your EAS updates were not signed. EAS code signing generated an RSA key pair of 2,048 bits in PEM files, which signing-key add takes as it is:

npx hotcodepush signing-key add --private-key-path ../keys/private-key.pem

It registers the public key with the app and appends it to publicKeys in hotcodepush.json. The build step writes the list into the binary, which is what updates.codeSigningCertificate did. 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 EAS channels had

production exists. Each further EAS channel becomes one here:

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 profile that followed staging sets HOTCODEPUSH_CHANNEL under env in eas.json, as Set up environments and tenants shows.

6. Prebuild and ship the store release

npx expo prebuild --clean

Then build the release as before. The build step runs binary create inside the build and registers it, so the machine, EAS Build included, needs HOTCODEPUSH_TOKEN in the build's environment or npx hotcodepush login. Under CI a store build without a token fails, and npx hotcodepush binary list shows the registered builds.

A development build takes no updates: verify in a release build, then run npx hotcodepush doctor.

7. 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 expo export:embed per platform where it runs, so the EXPO_PUBLIC_* values an EAS environment gave eas update must be in the job's environment. 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.

8. Run both until the fleet moves

A binary carries one client: an EAS update never reaches a HotCodePush build, nor the reverse. Hotfixes for the old builds come from the branch of step 1 with eas update, and their runtime version must match exactly. The Binaries page and binary list show each new build's devices.

Old binaries stay on EAS until their users update

A user who never updates the binary stays on EAS and never receives a HotCodePush release. Once the old builds are below the share you leave behind, run eas channel:pause production: their devices keep their update, and EAS counts no new MAU. Never run eas update:roll-back-to-embedded, which sends them back to older code.

CLI commands

EAS CLIHotCodePush
eas update:configurehotcodepush init
eas update --channel production --message "Fixes the cart"release create --channel production --notes "Fixes the cart". The notes ride the public index, and the commit is recorded on its own.
eas update --autorelease create --bundle-version "$(git rev-parse --short HEAD)"
eas update --branch stagingNothing. A channel has no branch, and release create --from-channel staging --channel production promotes.
eas update --platform iosrelease create --platform ios
eas update --environment productionNothing. The bundle is built on the machine that uploads, with its environment.
eas update --rollout-percentage 25release create --rollout-percent 25
eas update --private-key-path keys/private-key.pemrelease create --private-key-path keys/private-key.pem, or the key's text in HOTCODEPUSH_SIGNING_KEY
eas update --input-dir dist --skip-bundlerrelease create --path <dir> takes one platform's expo export:embed output, never an expo export directory.
eas update --upload-source-mapsNothing to pass. The upload leaves maps out, and Tag releases in your telemetry uploads them to your tracker.
eas update:edit --rollout-percentage 50release rollout --release 43 --rollout-percent 50, once per platform's number
eas update:revert-update-rolloutrelease revoke --release 43, or release pause --release 43 to leave its devices on it, once per platform's number
eas update:rollbackrelease rollback --to-release 42, once per platform's number
eas update:republish --group <id>release rollback --to-release 12, once per platform's number
eas update:republish --destination-channel productionrelease create --from-channel staging --channel production
eas update:roll-back-to-embeddedrelease revoke --all
eas update:list, update:view, update:insightsrelease list, release get, statistics get --type updates
eas channel:create, channel:list, channel:view, channel:deletechannel create, channel list, channel get, channel delete
eas channel:pause, channel:resumechannel pause, channel resume
eas channel:protect, channel:unprotectchannel update --protected, channel update --protected false
eas channel:edit --branch, channel:rollout, eas branch:*Nothing. A channel has no branch, and a rollout belongs to a release.
EXPO_TOKENHOTCODEPUSH_TOKEN in the environment, or hotcodepush login
expo/expo-github-action with its preview subactionA run: step runs npx hotcodepush release create (GitHub Actions), and the preview channel workflow replaces the preview.

A release is numbered per channel and, on Expo, exists per platform: one release create makes two, one per expo export:embed output. The CLI reference lists every command, release and its flags on the release page.

SDK calls

expo-updatesHotCodePushNote
Updates.checkForUpdateAsync()checkForUpdate()It answers available or up_to_date. Nothing is downloaded.
Updates.fetchUpdateAsync()downloadUpdate({ applyStrategy: 'manual' })Without the option the update applies per applyStrategy.
Updates.reloadAsync()applyUpdate()It answers nothing_to_apply when no update waits.
check, fetch, then reloadsync({ applyStrategy: 'immediate' })One cycle gives one typed result.
useUpdates()useUpdates()state is getState(), lastSync the last cycle's result, isSyncing and downloadProgress from 0 to 1.
currentlyRunning.updateId, createdAt, manifestgetState().currentReleaseIt is null on the embedded bundle.
Updates.channel, Updates.runtimeVersiongetChannel(), getDevice().fingerprint
Updates.isEmergencyLaunchnotifyReady().isRolledBack, the updateRolledBack event
Updates.setUpdateRequestHeadersOverride()setChannel({ name }), setChannel(null)The choice is persisted, and the channel must be discoverable.
Updates.setUpdateURLAndRequestHeadersOverride()setChannel({ id })There is no other URL and no update id.
Updates.setExtraParamAsync()setAttributes()--attribute conditions match them.
Updates.readLogEntriesAsync()getState().lastCheck, showDebugScreen()
useUpdateEvents(), addListener(), removed from expo-updatesaddListener()updateAvailable, updateDownloaded, updateFailed, downloadProgress, updateRolledBack
checkAutomatically: "ON_LOAD"checkStrategy: autoThis is the default. It also checks on resume and every checkIntervalSeconds, 900 by default.
checkAutomatically: "NEVER"checkStrategy: manual
checkAutomatically: "ON_ERROR_RECOVERY"nonemanual is the closest.
checkAutomatically: "WIFI_ONLY"downloadStrategy: unmeteredEAS gates the check, HotCodePush the download.
fallbackToCacheTimeout: 0applyStrategy: next_startThis is the default in both.
fallbackToCacheTimeout above 0applyStrategy: immediateIt reloads once downloaded and rendered instead of holding the launch screen.
codeSigningCertificate, codeSigningMetadatapublicKeys in hotcodepush.jsonsigning-key add fills it, with the same algorithm.
enableBsdiffPatchSupportnoneDeltas are always on.
maxUpdatesToKeepnoneThe SDK keeps the current, the next and the fallback release.
useEmbeddedUpdate, useNativeDebugnone

The Expo SDK's API page has every method, event and type. Choose an apply strategy compares the strategies fallbackToCacheTimeout maps to.

Before and after

The updates block and runtimeVersion leave the app config, and the plugin entry arrives:

app.json
{
  "expo": {
    "runtimeVersion": { "policy": "appVersion" }, 
    "updates": { 
      "url": "https://u.expo.dev/<projectId>", 
      "checkAutomatically": "ON_LOAD", 
      "fallbackToCacheTimeout": 0
    }, 
    "plugins": ["@hotcodepush/expo-ota-updates"], 
    "extra": { "eas": { "projectId": "<projectId>" } }
  }
}

hotcodepush.json takes the address and the channel:

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

production needs nothing in eas.json, as hotcodepush.json names it. Another profile sets its channel through the environment instead of the channel key:

eas.json
{
  "build": {
    "staging": {
      "channel": "staging"
      "env": { "HOTCODEPUSH_CHANNEL": "staging" } 
    }
  }
}

What differs

The fingerprint does not read the app config

The fingerprint hashes the lockfile's native packages and the files under extraFingerprintPaths, never app.json. Expo's fingerprint policy read the app config too, so a changed plugin option or permission moved the runtime version. Here it moves nothing unless app.json is listed in extraFingerprintPaths. Then every edit of the file does, version bumps included.

MAU is counted differently

EAS counts an install that downloaded at least one update in the month, while HotCodePush counts a device that checked for one (Billing and MAU). The two numbers are not comparable.

  • Addresses: updates.url and projectId become appId, which is public, and a channel is addressed by name in the build and by an unguessable id at runtime.
  • The fingerprint replaces the runtime version: it rides on every release, so a release never lands on a build whose native code differs. Nothing is bumped, and a --binary-version range over the store version narrows it. See Compatibility.
  • A rollout belongs to a release, not a branch: nothing blocks the next publish, and a newer release supersedes the rollout inside its bucket. A device keeps the release it has, and auto-pause halts one whose failure rate climbs. See Roll out gradually.
  • Rollback is a release: release rollback --to-release <number> publishes an earlier bundle as a new release, mandatory by default, once per platform's number. A full revoke, release revoke --all, returns the channel's devices to the embedded bundle across every native layer.
  • The readiness gate has no off switch: a release that has not rendered within readyTimeoutSeconds, 10 by default, is rolled back and never tried there again. The report feeds auto-pause.
  • Mandatory updates exist: release create --mandatory applies at once under mandatoryApplyStrategy: immediate.
  • Code signing is on every plan.

Next steps

  • Ship every commit has the pipeline that releases every merge with the one CLI line, named by its commit.
  • Test with preview channels gives every pull request an expiring channel and a QR code for testers, replacing the development build's preview.
  • Roll out gradually: start a release at a small percentage, let it progress and let auto-pause stop a bad one.
  • Expo SDK reference documents every hotcodepush.json key, method, event, type and reason of the SDK you installed.

Last updated on

On this page