HotCodePush

Migrate from Capawesome Cloud

Decide whether to stay, move or combine, then replace the Capawesome plugin with the HotCodePush Capacitor SDK at your next store release.

A Capacitor app moves its over-the-air (OTA) updates from Capawesome Cloud to HotCodePush by replacing @capawesome/capacitor-live-update with @hotcodepush/capacitor-live-updates at its next store release. That store build is the switch, and older builds keep taking Capawesome Cloud bundles until their users move. What differs, side by side, is on the site's Capawesome Cloud alternative page.

Which product fits

Stay on Capawesome Cloud when you want native builds, store publishing and updates in one console on one plan. Stay too for Electron, Ionic Portals, self-hosted bundles, a reverse proxy or Capacitor 6, which HotCodePush lacks.

Move when the OTA updates are your reason to move. HotCodePush adds a readiness gate with rollback on by default, the fingerprint instead of a channel per native release, auto-pausing rollouts and attribute targeting. Which of the two products is for you compares them.

Combine them by keeping the native builds and the store deployments in Capawesome Cloud and dropping the web builds, the channel deployments and the plugin. Capawesome Cloud is that pipeline, and the rest of this page moves the updates to HotCodePush.

Install the HotCodePush skills:

npx skills add hotcodepush-team/skills

Then paste the prompt into your coding agent:

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

The agent removes the Capawesome plugin, runs init, maps channels, config keys, SDK calls and binary ranges, and reports a checklist for the store release. It never touches Capawesome Cloud, 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 Capacitor app on a supported major, listed on Supported versions. An app on the plugin's v6-lts line upgrades Capacitor first.
  • A store release lies ahead, since the swap ships in a native build.
  • Node.js is on every machine that makes a native build, a Capawesome Cloud build included, since the build step runs the CLI on that machine.

Steps

If you already removed the plugin and ran init, skip to step 4.

1. Freeze a branch for the old builds

git branch capawesome-cloud-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 plugin, and private.pem stays where that pipeline reads it.

2. Remove the Capawesome plugin

npm uninstall @capawesome/capacitor-live-update

Delete the LiveUpdate block from plugins in capacitor.config.ts and the /// <reference types="@capawesome/capacitor-live-update" /> line, then CapawesomeLiveUpdateDefaultChannel from Info.plist and capawesome_live_update_default_channel from build.gradle. Replace every call and listener per the SDK calls below, then run npx cap sync.

3. Run init

npx hotcodepush init

init signs you in, picks or creates the organization and the app with its production channel, installs @hotcodepush/capacitor-live-updates, writes hotcodepush.json and wires the build step. The Capacitor SDK's Installation page has the manual steps.

It asks whether to generate a signing key, which Sign your bundles explains. A signing app declines and keeps its pair: npx hotcodepush signing-key add --private-key-path private.pem registers the public key and writes publicKeys. The publicKey string itself does not carry over. Decline the first release for now.

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.

Then import the package in the app's entry file, where ready() was, even when no call follows:

src/main.ts
import '@hotcodepush/capacitor-live-updates';

The import reports the first render, which is what ready() did. An app that imports the package late, or not at all, confirms no update, and every one is rolled back after readyTimeoutSeconds.

4. Create the channels your Capawesome channels had

production exists and takes the place of default. The versioned channels, production-<versionCode>, collapse into it: the fingerprint keeps a release away from a build it was not made for. Each further channel becomes one here:

npx hotcodepush channel create --name beta --discoverable

--discoverable lets setChannel({ name }) find the channel by name, as Channels describes. Without it the channel is reachable by its id alone. A build that set defaultChannel or the native key sets channel in hotcodepush.json or HOTCODEPUSH_CHANNEL instead, as Set up environments and tenants shows.

5. Ship the store release that carries the SDK

Build and submit the release as before. The build step runs binary create inside the build and registers it with its fingerprint, so the machine needs npx hotcodepush login or HOTCODEPUSH_TOKEN. A Capawesome Cloud build takes the token as an environment secret, as Capawesome Cloud shows, and under CI a build without one fails.

6. Release from your pipeline

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

This line replaces npx @capawesome/cli apps:liveupdates:upload --app-id <id> --path dist --channel production or the cloud-live-update-action step. GitHub Actions has the CLI step. With HOTCODEPUSH_TOKEN and, for a signing app, HOTCODEPUSH_SIGNING_KEY in the environment, it is the whole pipeline step. The command reads webDir from capacitor.config, and Ship every commit is the pipeline it 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 plugin: a Capawesome Cloud bundle never reaches a HotCodePush build, and the reverse. Keep the Capawesome Cloud pipeline while old builds have users. Its console's MAU shows how many still check in. Then end the plan, unless the native builds still run on it.

Never upload a web build without ready() to Capawesome Cloud

An old build with readyTimeout set rolls a bundle back when ready() never runs, and a migrated bundle never calls it. The branch of step 1 keeps the call.

CLI commands

Capawesome CLIHotCodePush
login --token, CAPAWESOME_TOKENlogin, HOTCODEPUSH_TOKEN
apps:createinit, or app create
apps:update --channel, --channel-discoveryNone. The build's channel picks it, and channel update --discoverable flags one channel.
apps:liveupdates:upload --path dist --channel productionrelease create --path dist --channel production
upload without --channelrelease create releases to hotcodepush.json's channel, and bundle upload uploads without releasing.
--channel repeated on apps:liveupdates:create--channel production --channel beta
--rollout-percentage 10--rollout-percent 10
--android-min, --android-max, --ios-min, --ios-max--binary-version ">=2.3.0" ranges over the store version, and the fingerprint rides every release on its own.
--android-eq 5, --ios-eq 5, which exclude that build--binary-version "<2.4.1.5 || >2.4.1.5" excludes it and is usually dropped. See Compatibility.
--private-key private.pem--private-key-path private.pem, or the key's text in HOTCODEPUSH_SIGNING_KEY
--git-refThe ref is detected, and --git-ref, --git-sha and --git-message override it.
--custom-property key=value--notes "text". The notes ride the public index.
--artifact-type manifest, apps:liveupdates:generatemanifestNothing to pass. Deltas are always on.
--expires-in-daysNone. Retention covers it.
apps:liveupdates:create, apps:builds:create --platform webyour own npm run build, then release create
apps:liveupdates:register --urlnone
apps:deployments:create --build-number 42 --channel betarelease create --bundle 17 --channel beta, or --from-channel staging
apps:liveupdates:rollout --channel production --percentage 50release rollout --release 43 --rollout-percent 50
apps:liveupdates:rollback --channel production --steps 1release rollback --channel production, or --to-release 41
apps:liveupdates:setnativeversionsNone. Create a new release with --binary-version, then release revoke the old one.
apps:liveupdates:generatesigningkeysigning-key create, or signing-key add --private-key-path private.pem for the pair you have
apps:channels:create --name beta --ignore-errorschannel create --name beta --if-not-exists
apps:channels:create --protectedA registered signing key refuses every unsigned upload. --protected here limits changes to Owners and Admins.
apps:channels:update --channel-id <id> --namechannel update --channel <id|name> --name
apps:channels:pause, resume, get, listchannel pause, resume, get, list
apps:channels:delete, which keeps the bundleschannel delete deletes the channel's releases too.
apps:devices:forcechannel, unforcechannelNone. Use setChannel() in the app, or release create --device <id>.
apps:devices:probe --device-id, apps:devices:deletedevice probe --device, device delete --device
cloud-live-update-action with appId, token, channel, path, privateKey, rolloutPercentageA run: step runs npx hotcodepush release create with --channel, --path and --rollout-percent, with HOTCODEPUSH_TOKEN and HOTCODEPUSH_SIGNING_KEY in its env. The app comes from hotcodepush.json.

A bundle is numbered per app with a free version label, and a release puts it on a channel, numbered per channel (Bundles and releases). The CLI reference lists every command.

What maps to what

plugins.LiveUpdatehotcodepush.jsonNote
appIdappIdThis is the HotCodePush app id, not Capawesome's. init writes it.
defaultChannel, CapawesomeLiveUpdateDefaultChannel, capawesome_live_update_default_channelchannel, or HOTCODEPUSH_CHANNEL per buildThe build step resolves it to the channel's id, and a versioned name collapses to production.
autoUpdateStrategy: 'background'checkStrategy: auto with applyStrategy: next_startBoth are defaults. A check every checkIntervalSeconds in the foreground, 900 by default, is added.
autoUpdateStrategy: 'none', the defaultcheckStrategy: manualsync() starts each cycle.
readyTimeout: 10000readyTimeoutSeconds: 10It is in seconds and the default, with no 0 to turn it off.
autoBlockRolledBackBundlesnoneA bundle that failed is always blocked on the device.
autoDeleteBundlesnoneThe SDK keeps the current, the next and the fallback release.
publicKey, one PEM without line breakspublicKeysEach entry is rsa-v1_5-sha256: and the key's SPKI DER in base64, written by signing-key create or add.
httpTimeout, serverDomainnone
defaultChannel in capacitor.electron.config.tsnoneHotCodePush ships for iOS and Android only.

The Capacitor SDK's Configuration page lists every key with its type and default.

SDK calls

CapawesomeHotCodePushNote
ready()Nothing. The import in the entry file is the signal.notifyReady() under readySignal: manual answers isRolledBack and previousRelease.
sync()sync()It applies per applyStrategy. sync({ applyStrategy: 'immediate' }) replaces sync() followed by reload().
sync({ channel }), fetchLatestBundle({ channel })setChannel({ name }), then sync()The choice is persisted, with no channel per call.
fetchLatestBundle()checkForUpdate()It answers available with the release, its notes and the pack's size. Nothing is downloaded.
downloadBundle({ url, bundleId }), then setNextBundle()downloadUpdate({ applyStrategy: 'manual' })The SDK downloads from the index, never from a URL.
reload()applyUpdate()It reloads only when an update waits.
reset(), setNextBundle({ bundleId: null })clearUpdates()It reloads onto the embedded bundle itself.
getCurrentBundle(), getNextBundle()getState()currentRelease, nextRelease
getDownloadedBundles(), deleteBundle()noneThe SDK keeps the current, the next and the fallback release.
getBlockedBundles()getState().failedBundleIds
clearBlockedBundles()clearUpdates()It also drops the downloads and reloads.
setChannel({ channel })setChannel({ name }), or { id }A name needs the channel flagged discoverable.
setChannel({ channel: null })setChannel(null)
getChannel()getChannel()It answers id, source and name, which is null for the build's channel.
fetchChannels()noneName the channels in the app or serve them from your backend.
setCustomId(), getCustomId()setAttributes({ userId }), getDevice().attributesAttributes are labels, never personal data.
getDeviceId(), getVersionCode(), getVersionName()getDevice()id, binaryBuild, binaryVersion
getConfig(), setConfig({ appId }), resetConfig()noneA tenant's channel comes through setChannel({ id }).
isSyncing()noneA second sync() joins the running cycle.
downloadBundleProgress, progress from 0 to 1downloadProgress, progress from 0 to 1releaseId, progressBytes, downloadSizeBytes
nextBundleSetupdateDownloadedrelease and applyStrategy
reloadednone
ready().rollbackupdateRolledBackIt fires at the start that follows the rollback.
nonerollbackUpdate(), setRestartAllowed(), showDebugScreen(), updateAvailable, updateFailed
the Always Latest pattern: 'background', nextBundleSet, reload()applyStrategy: manual, updateDownloaded, applyUpdate()Step 3 of Choose an apply strategy builds it.
the Force Update pattern: the splash screen held, sync(), reload()sync({ applyStrategy: 'immediate' })Hide the splash screen on any status but applied, since an immediate reload waits for the first render.
the Instant pattern: a silent push, sync(), reload()sync({ applyStrategy: 'immediate' }) from the push handler
a channel derived from the version codenothingThe fingerprint rides on every release instead.

The Capacitor SDK's API page has every method, event and type. On the web every method rejects with Capacitor's UNIMPLEMENTED error, as the plugin's did, so the Capacitor.isNativePlatform() guards carry over.

Before and after

The LiveUpdate block leaves capacitor.config.ts:

capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'Example',
  webDir: 'dist',
  plugins: { 
    LiveUpdate: { 
      appId: '00000000-0000-0000-0000-000000000000', 
      autoUpdateStrategy: 'background', 
      defaultChannel: 'production', 
      readyTimeout: 10000, 
    }, 
  }, 
};

export default config;

hotcodepush.json takes its place at the project root. The background strategy equals the defaults, so two keys are the whole file:

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

ready() goes, and the import takes its place in the entry file:

src/main.ts
import { LiveUpdate } from '@capawesome/capacitor-live-update'; 
import '@hotcodepush/capacitor-live-updates'; 

await LiveUpdate.ready(); 

What differs

  • The readiness gate is always on: the plugin's readyTimeout was off unless you set it. Here readyTimeoutSeconds is 10 by default and has no off switch (Configuration).
  • A rollback lands on the last confirmed release, or on the embedded bundle without one. It blocks the failed bundle on that device (Roll back a release).
  • release rollback is mandatory by default: running apps restart as soon as they have it, where a rollback deployment applied at the next launch. Pass --mandatory false, or set mandatoryApplyStrategy: manual, to let them wait.
  • Protected means a role, not a signature: --protected limits changes to Owners and Admins, and a registered key refuses every unsigned upload for the app.
  • No server-side default channel and no forced channel: the build carries its channel, and HotCodePush never assigns one. A tester switches with setChannel() or a link (Switch channels by link), and release create --device <id> targets one device.
  • No channel per call, and a name needs --discoverable: sync({ channel }) has no counterpart, and anyone holding the app id can read a discoverable name.
  • getChannel().name is null on the build's channel: the build carries the channel by id alone, so code that compared the name reads id or source instead.
  • The fingerprint replaces versioned channels and per-platform build ranges: a release never lands on a build whose native code differs. A --binary-version range compares the store version, with the build number as its fourth component, for both platforms at once. See Compatibility.
  • A bundle is at most 512 MB, where Capawesome Cloud took 1 GB. Retention is 90 days on every plan. Limits and fair use lists every limit.
  • Gone with no replacement: custom properties, fetchChannels(), setConfig({ appId }) and cloud web builds have no counterpart.

Next steps

  • Roll out gradually: start a release small, let it progress and let auto-pause stop a bad one.
  • Sign your bundles covers the pair you brought along, and the store build that refuses an unsigned release.
  • Capawesome Cloud runs the native builds and the store deployments beside the OTA updates from HotCodePush.
  • Capacitor SDK reference documents every hotcodepush.json key, method, event, type and reason of the SDK you installed.

Last updated on

On this page