---
description: The complete guide to Cordova Live Updates — how OTA updates work, update strategies, code signing, best practices, and a real-world end-to-end example.
title: Cordova Live Updates: A Complete Guide to OTA Updates - Capawesome
image: https://capawesome.io/docs/assets/images/social/blog/cordova-live-updates-guide.png
---

[ Skip to content](#cordova-live-updates-a-complete-guide-to-ota-updates) 

[ 🔐 Introducing the **Capacitor Vault** plugin — store secrets behind biometrics or a device passcode.](/blog/announcing-the-capacitor-vault-plugin/) 

* [  SDKs ](/docs/sdks/)
* [  Formbricks ](/docs/sdks/capacitor/formbricks/)
* [  Geocoder ](/docs/sdks/capacitor/geocoder/)
* [  Google Sign-In ](/docs/sdks/capacitor/google-sign-in/)
* [  Grafana Faro ](/docs/sdks/capacitor/grafana-faro/)
* [  libSQL ](/docs/sdks/capacitor/libsql/)
* [  Live Update ](/docs/sdks/capacitor/live-update/)
* [  Managed Configurations ](/docs/sdks/capacitor/managed-configurations/)
* [  Media Session ](/docs/sdks/capacitor/media-session/)
* [  ML Kit ](/docs/sdks/capacitor/mlkit/)
* [  Navigation Bar ](/docs/sdks/capacitor/navigation-bar/)
* [  NFC ](/docs/sdks/capacitor/nfc/)
* [  OAuth ](/docs/sdks/capacitor/oauth/)
* [  Pedometer ](/docs/sdks/capacitor/pedometer/)
* [  Photo Editor ](/docs/sdks/capacitor/photo-editor/)
* [  PostHog ](/docs/sdks/capacitor/posthog/)
* [  Printer ](/docs/sdks/capacitor/printer/)
* [  Purchases ](/docs/sdks/capacitor/purchases/)
* [  RealtimeKit ](/docs/sdks/capacitor/realtimekit/)
* [  Screen Orientation ](/docs/sdks/capacitor/screen-orientation/)
* [  Screenshot ](/docs/sdks/capacitor/screenshot/)
* [  Secure Preferences ](/docs/sdks/capacitor/secure-preferences/)
* [  Speech Recognition ](/docs/sdks/capacitor/speech-recognition/)
* [  Speech Synthesis ](/docs/sdks/capacitor/speech-synthesis/)
* [  Share Target ](/docs/sdks/capacitor/share-target/)
* [  Square Mobile Payments ](/docs/sdks/capacitor/square-mobile-payments/)
* [  SQLite ](/docs/sdks/capacitor/sqlite/)
* [  Superwall ](/docs/sdks/capacitor/superwall/)
* [  Torch ](/docs/sdks/capacitor/torch/)
* [  Vault ](/docs/sdks/capacitor/vault/)
* [  Wifi ](/docs/sdks/capacitor/wifi/)
* [  Zip ](/docs/sdks/capacitor/zip/)
* [  Cordova ](/docs/sdks/cordova/)
* [  Cloud ](/docs/cloud/)
* [  Integrations ](/docs/cloud/live-updates/integrations/)
* Concepts
* Reference
* [  Troubleshooting ](/docs/cloud/live-updates/troubleshooting/)
* [  FAQ ](/docs/cloud/live-updates/faq/)
* [  Native Builds ](/docs/cloud/native-builds/)
* [  Set Up Environments ](/docs/cloud/native-builds/environments/)
* [  Overwrite Native Configurations ](/docs/cloud/native-builds/native-configurations/)
* [  Auto-Increment Build Numbers ](/docs/cloud/native-builds/auto-incrementing-build-numbers/)
* [  Configure the Web Build Script ](/docs/cloud/native-builds/web-build-script/)
* [  Build from a Monorepo ](/docs/cloud/native-builds/monorepo/)
* [  Use pnpm or Yarn ](/docs/cloud/native-builds/package-managers/)
* [  Install Private npm Packages ](/docs/cloud/native-builds/npm-private-registry/)
* [  Override the Java Version ](/docs/cloud/native-builds/override-java-version/)
* [  Custom iOS Provisioning Profiles ](/docs/cloud/native-builds/custom-ios-provisioning-profiles/)
* [  Build without Git ](/docs/cloud/native-builds/build-without-git/)
* [  Access Git Behind a Firewall ](/docs/cloud/native-builds/firewall-access/)
* [  Integrations ](/docs/cloud/native-builds/integrations/)
* Reference
* [  Troubleshooting ](/docs/cloud/native-builds/troubleshooting/)
* [  FAQ ](/docs/cloud/native-builds/faq/)
* [  App Store Publishing ](/docs/cloud/app-store-publishing/)
* [  Submit a Build ](/docs/cloud/app-store-publishing/submit-a-build/)
* [  Submit Automatically After a Build ](/docs/cloud/app-store-publishing/submit-automatically/)
* [  Troubleshooting ](/docs/cloud/app-store-publishing/troubleshooting/)
* [  FAQ ](/docs/cloud/app-store-publishing/faq/)
* [  Automations ](/docs/cloud/automations/)
* [  Reference ](/docs/cloud/automations/reference/)
* [  Troubleshooting ](/docs/cloud/automations/troubleshooting/)
* [  FAQ ](/docs/cloud/automations/faq/)
* [  Assist ](/docs/cloud/assist/)
* [  CLI ](/docs/cloud/cli/)
* APIs and SDKs
* [  Webhooks ](/docs/cloud/webhooks/)
* [  Integrations ](/docs/cloud/integrations/)
* Account
* [  Organization ](/docs/cloud/organizations/)
* [  Two-Factor Enforcement ](/docs/cloud/organizations/two-factor-authentication/)
* [  Audit Logs ](/docs/cloud/organizations/audit-logs/)
* [  Billing ](/docs/cloud/organizations/billing/)
* [  License Keys ](/docs/cloud/license-keys/)
* [  AI ](/docs/ai/)
* [  Insiders ](/docs/insiders/)
* [  Billing & Plans ](/docs/insiders/billing-and-plans/)
* [  FAQ ](/docs/insiders/faq/)
* [  License ](https://capawesome.io/legal/eula/)
* [  Support ](/docs/support/)
* [  Contributing ](/docs/contributing/)
* Contributing code
* [  Code of Conduct ](/docs/contributing/code-of-conduct/)
* [  Questions ](https://docs.github.com/en/discussions/collaborating-with-your-community-using-discussions/participating-in-a-discussion#creating-a-discussion)
* [  Blog ](/blog/)
* Categories

* [  Wiring It Up ](#wiring-it-up)
* [  Three Choices to Make ](#three-choices-to-make)
* [  Decision 2: Versioning Strategy ](#decision-2-versioning-strategy)
* [  Decision 3: Update Delivery Method ](#decision-3-update-delivery-method)
* [  Security: Code Signing ](#security-code-signing)
* [  Best Practices ](#best-practices)
* [  End-to-End Example: A Real-World Setup ](#end-to-end-example-a-real-world-setup)
* [  Get Started ](#get-started)
* [  Conclusion ](#conclusion)

* Related links

# Cordova Live Updates: A Complete Guide to OTA Updates[¶](#cordova-live-updates-a-complete-guide-to-ota-updates "Permanent link")

For most Cordova apps, the day-to-day changes — a copy fix, a styling tweak, a bug in the JavaScript — live entirely in the web assets. Shipping one has traditionally meant rebuilding the native binary, resubmitting to the App Store and Google Play, and waiting out review before anyone sees it. Cordova Live Updates skip that round trip. You publish a new web bundle, and devices download and apply it on their next launch — usually within minutes, without a store submission in the loop.

This guide covers the whole picture: how an over-the-air update moves through a Cordova app, the three decisions that shape your setup, how code signing protects the bundle in transit, the practices that keep production rollouts boring, and a working example you can clone today. It assumes you're either replacing Appflow's live updates or adding OTA delivery to a Cordova app for the first time.

## What Are Live Updates?[¶](#what-are-live-updates "Permanent link")

A Cordova app has two layers. The **native layer** is the compiled binary that's installed from the App Store or Google Play — it contains the WebView, the Cordova plugins, and the platform glue. The **web layer** is everything that runs inside that WebView: your HTML, CSS, JavaScript, and static assets.

![Layers](/docs/assets/images/diagrams/live-update-layers-light.png#only-light) ![Layers](/docs/assets/images/diagrams/live-update-layers-dark.png#only-dark) 

Cordova App Layers

A live update is an Over-the-Air (OTA) update of the web layer only. The native binary stays untouched. Instead of pushing a new build to the stores, you upload a new web bundle to a delivery service, and the [Cordova Live Update plugin](/docs/sdks/cordova/live-update/) in your app downloads it, swaps it in, and reloads.

That's the whole idea. The constraint is also the feature: because nothing native changes, you don't need an app store review and you don't need users to take any action.

### Why Use Live Updates?[¶](#why-use-live-updates "Permanent link")

Over-the-air updates earn their place in a Cordova workflow for a handful of concrete reasons:

* **Patch production without an app review.** A broken login, a wrong endpoint, a checkout regression — fixes like these reach users in minutes instead of sitting in a review queue for days.
* **Release on your own cadence.** Ship a small improvement the moment it's ready rather than bundling it into the next native submission.
* **Roll out gradually and experiment.** Send a bundle to a slice of your install base first, watch your dashboards, then widen it — or run separate bundles for beta, internal, and regional audiences.
* **Skip the manual-update gap.** You're no longer waiting on users who leave store updates switched off for weeks before they see your latest code.

### Binary-Compatible vs. Non-Binary-Compatible Changes[¶](#binary-compatible-vs-non-binary-compatible-changes "Permanent link")

The single most important rule with live updates: **you can only update what already exists in the native binary.** Changes that don't touch native code are called _binary-compatible_. Anything else needs a real app store release.

| Change                                            | Binary-compatible? | Where it ships |
| ------------------------------------------------- | ------------------ | -------------- |
| HTML, CSS, JavaScript                             | Yes                | Live update    |
| Images, fonts, JSON, web-only assets              | Yes                | Live update    |
| Web framework upgrade (Angular, React, Vue, etc.) | Yes                | Live update    |
| Cordova plugin added or removed                   | No                 | App store      |
| Cordova plugin major version bump                 | Usually no         | App store      |
| Native code (Java/Kotlin/Swift/Objective-C)       | No                 | App store      |
| AndroidManifest.xml, Info.plist, entitlements     | No                 | App store      |
| App icon, splash screen                           | No                 | App store      |

A useful mental model: if your change only modifies files inside your web project — your `www/` output — and doesn't add, remove, or upgrade a Cordova plugin or any native code or config, it's binary-compatible. We'll come back to this when we talk about versioning, because the most common production bug with live updates is delivering a bundle that depends on a native feature the installed binary doesn't have yet.

### How Do Cordova Live Updates Work Under the Hood?[¶](#how-do-cordova-live-updates-work-under-the-hood "Permanent link")

A Cordova WebView doesn't load your app from `https://example.com`. It loads it from a local origin served out of the app bundle — `https://localhost/` on Android (via `WebViewAssetLoader`) and `app://localhost/` on iOS (via `WKURLSchemeHandler`). By default, that content is the `www/` folder bundled inside the native app, which is exactly the output of your web build at the time you shipped the binary.

A live update swaps the folder those requests are served from:

1. The plugin downloads a new bundle and writes it to a local directory inside the app's sandbox.
2. It records that bundle as the _next_ bundle to use.
3. On the next launch — or immediately, if you call `reload()` — the WebView serves your app from the new bundle instead of the built-in `www/` folder.

One detail Cordova developers will appreciate: the plugin works with the **stock Cordova WebView**. Unlike Appflow's live update plugin, it does not require `cordova-plugin-ionic-webview`. It hooks into Cordova's official scheme handlers on `cordova-android` 10+ and `cordova-ios` 6+, so there's nothing extra to install on the native side. The one thing to avoid is forcing the legacy file scheme — setting `AndroidInsecureFileModeEnabled` to `true` or `Scheme` to `file` bypasses the handlers the plugin relies on, and updates won't apply.

### Are Live Updates Compliant With App Store Policies?[¶](#are-live-updates-compliant-with-app-store-policies "Permanent link")

Yes — and the reason is built into how both stores define the rule. Each one explicitly carves out interpreted code running inside a WebView, which is precisely what a live update ships.

On the Apple side, **App Store Review Guideline 3.3.2** permits downloading interpreted code provided it doesn't change the app's primary purpose, doesn't turn the app into a storefront for other code, and doesn't work around the platform's sandbox or signing. JavaScript executing in a `WKWebView` clears all three conditions.

Google is just as explicit. Its **Device and Network Abuse policy** bans apps that replace or modify themselves outside of Google Play, then names an exception for "code that is interpreted in a virtual machine or runtime (like JavaScript in a webview or browser)."

The common thread is that both stores object to _native_ self-modification, not to refreshing web content. A live update never touches the native binary, so it stays well inside the rules.

## Wiring It Up[¶](#wiring-it-up "Permanent link")

Before any choices, the plugin needs to be installed and connected to Capawesome Cloud. First, create an app in Capawesome Cloud — the CLI prompts for the organization and app name, and prints the resulting app ID:

`[](#%5F%5Fcodelineno-0-1)npx @capawesome/cli apps:create --type cordova
`

Then install the plugin, passing the app ID you just got back as a variable:

`[](#%5F%5Fcodelineno-1-1)cordova plugin add @capawesome/cordova-live-update --variable APP_ID=00000000-0000-0000-0000-000000000000
`

You configure the rest of the plugin through preferences in your `config.xml`. The `background` strategy checks for updates on launch and resume and applies them on the next start, while `READY_TIMEOUT` together with `AUTO_BLOCK_ROLLED_BACK_BUNDLES` enables automatic rollback:

`[](#%5F%5Fcodelineno-2-1)<preference name="APP_ID" value="00000000-0000-0000-0000-000000000000" />
[](#%5F%5Fcodelineno-2-2)<preference name="AUTO_UPDATE_STRATEGY" value="background" />
[](#%5F%5Fcodelineno-2-3)<preference name="AUTO_BLOCK_ROLLED_BACK_BUNDLES" value="true" />
[](#%5F%5Fcodelineno-2-4)<preference name="READY_TIMEOUT" value="10000" />
`

For full setup instructions, including the self-hosting and runtime configuration options, see the [Live Updates setup guide](/docs/cloud/live-updates/setup/).

## Three Choices to Make[¶](#three-choices-to-make "Permanent link")

With the plugin installed, three choices shape how live updates behave in your app. Pick a default for each, and you have a production setup.

### Decision 1: Update Strategy[¶](#decision-1-update-strategy "Permanent link")

The _strategy_ decides **when** an update is downloaded, **when** it's applied, and **whether the user is involved.** It's as much a UX decision as a technical one — each option trades freshness against friction differently, and the right choice depends on how important it is that users are on the latest code versus how much you're willing to interrupt them to get them there.

The `AUTO_UPDATE_STRATEGY` preference has two values, `background` and `none`. Around those two, you can build four patterns ranging from "set it once and forget about it" to "fire an update across the install base on demand":

| Pattern                           | User-visible delay        | Network behavior       | Use case                                   |
| --------------------------------- | ------------------------- | ---------------------- | ------------------------------------------ |
| **Background**                    | Next cold start           | Lazy, opportunistic    | Silent updates between sessions            |
| **Always Latest** _(recommended)_ | One prompt after download | Lazy, opportunistic    | User is informed and chooses when to apply |
| **Force Update**                  | Blocks app start          | Eager, on every launch | Apps where stale code is genuinely unsafe  |
| **Instant**                       | Fires on push             | Reactive, on demand    | Critical hotfixes for a live incident      |

The sections below walk through each one in turn, with the config and code to wire it up.

#### Background[¶](#background "Permanent link")

The simplest option, with zero app-side code. You set it once in `config.xml`:

`[](#%5F%5Fcodelineno-3-1)<preference name="APP_ID" value="00000000-0000-0000-0000-000000000000" />
[](#%5F%5Fcodelineno-3-2)<preference name="AUTO_UPDATE_STRATEGY" value="background" />
`

With this preference set, the plugin automatically checks for updates on app start and on resume, downloads new bundles in the background, and applies them the next time the app launches. Users get the new version on their next cold start — no prompts, no waiting. There's a built-in 15-minute minimum between checks so the plugin can't be accidentally turned into a polling loop that drains the battery.

The tradeoff is that the update is invisible. Users won't know a new version exists until the app silently behaves differently on their next session, which can be confusing if something visibly changed. Reach for this pattern when the update doesn't need user attention.

#### Always Latest (Recommended)[¶](#always-latest-recommended "Permanent link")

Our recommended pattern. You keep the `background` strategy's silent download, but instead of making the user wait for the next cold start, you prompt them to apply the update as soon as it's ready. Users find out when something new is available, and they decide whether to apply it now or keep working with the current version.

You opt in by listening for the `nextBundleSet` event once Cordova is ready:

`[](#%5F%5Fcodelineno-4-1)document.addEventListener("deviceready", () => {
[](#%5F%5Fcodelineno-4-2)  cordova.plugins.LiveUpdate.addListener("nextBundleSet", async ({ bundleId }) => {
[](#%5F%5Fcodelineno-4-3)    if (!bundleId) {
[](#%5F%5Fcodelineno-4-4)      return;
[](#%5F%5Fcodelineno-4-5)    }
[](#%5F%5Fcodelineno-4-6)    const shouldReload = confirm("A new version is available. Install it now?");
[](#%5F%5Fcodelineno-4-7)    if (shouldReload) {
[](#%5F%5Fcodelineno-4-8)      await cordova.plugins.LiveUpdate.reload();
[](#%5F%5Fcodelineno-4-9)    }
[](#%5F%5Fcodelineno-4-10)  });
[](#%5F%5Fcodelineno-4-11)});
`

The download still happens in the background, so the prompt only appears once the bundle is fully on disk — there's no waiting on a network call when the user taps "install". This is the pattern the end-to-end example later in this guide uses.

#### Force Update[¶](#force-update "Permanent link")

The "user must be on the latest version before they can use the app" pattern. First, stop the splash screen from auto-hiding so it stays visible while the update check runs, and turn off the automatic strategy so you're in full control:

`[](#%5F%5Fcodelineno-5-1)<preference name="AutoHideSplashScreen" value="false" />
[](#%5F%5Fcodelineno-5-2)<preference name="AUTO_UPDATE_STRATEGY" value="none" />
`

Then on `deviceready`, sync and either reload (if there's a new bundle) or hide the splash screen (if there isn't):

`[](#%5F%5Fcodelineno-6-1)document.addEventListener("deviceready", async () => {
[](#%5F%5Fcodelineno-6-2)  const { nextBundleId } = await cordova.plugins.LiveUpdate.sync();
[](#%5F%5Fcodelineno-6-3)  if (nextBundleId) {
[](#%5F%5Fcodelineno-6-4)    await cordova.plugins.LiveUpdate.reload();
[](#%5F%5Fcodelineno-6-5)  } else {
[](#%5F%5Fcodelineno-6-6)    navigator.splashscreen.hide();
[](#%5F%5Fcodelineno-6-7)  }
[](#%5F%5Fcodelineno-6-8)});
`

This guarantees freshness, but it costs you. Every cold start on a slow connection means the user stares at the splash screen while a multi-megabyte bundle downloads. Reserve it for cases where running a stale version is genuinely unsafe (e.g. some kinds of regulated apps). Always set `AUTO_UPDATE_STRATEGY` to `none` here to avoid two update mechanisms running at once.

#### Instant[¶](#instant "Permanent link")

For genuinely critical updates — a bad bug already in production, a security patch — the fastest path is a silent push notification that tells the app "go check for an update _now_". With a push plugin such as `cordova-plugin-firebasex` wired up, the handler is small:

`[](#%5F%5Fcodelineno-7-1)push.on("notification", async (data) => {
[](#%5F%5Fcodelineno-7-2)  if (data.additionalData?.type !== "live-update") {
[](#%5F%5Fcodelineno-7-3)    return;
[](#%5F%5Fcodelineno-7-4)  }
[](#%5F%5Fcodelineno-7-5)  const { nextBundleId } = await cordova.plugins.LiveUpdate.sync();
[](#%5F%5Fcodelineno-7-6)  if (nextBundleId) {
[](#%5F%5Fcodelineno-7-7)    const shouldReload = confirm("Critical update available. Install now?");
[](#%5F%5Fcodelineno-7-8)    if (shouldReload) {
[](#%5F%5Fcodelineno-7-9)      await cordova.plugins.LiveUpdate.reload();
[](#%5F%5Fcodelineno-7-10)    }
[](#%5F%5Fcodelineno-7-11)  }
[](#%5F%5Fcodelineno-7-12)});
`

This requires push infrastructure and is meant for the long tail of "someone has the broken bundle open right now, and we need to fix it before they touch checkout." Set `AUTO_UPDATE_STRATEGY` to `none` and don't use this as your default pattern — it's a _break-glass_ tool, not an everyday flow.

### Decision 2: Versioning Strategy[¶](#decision-2-versioning-strategy "Permanent link")

This is the decision that prevents the most common production failure: **shipping a web bundle that doesn't match the installed native binary.**

Picture a user with version 5 of your native app. You ship a new web bundle that calls a method on a Cordova plugin you only added in version 6\. If that bundle reaches the v5 user, their app breaks on launch — and the only way out is reinstalling from the store.

You prevent this by binding each web bundle to the range of native versions it's compatible with. There are two ways to do it.

#### Versioned Bundles[¶](#versioned-bundles "Permanent link")

You attach min/max version ranges to each upload:

`[](#%5F%5Fcodelineno-8-1)npx @capawesome/cli apps:liveupdates:upload \
[](#%5F%5Fcodelineno-8-2)  --android-min 10 --android-max 12 \
[](#%5F%5Fcodelineno-8-3)  --ios-min 10 --ios-max 12
`

The Cloud only serves the bundle to devices inside the range. Simple, but error-prone: you have to remember the right flags on every upload, and rollouts get awkward because the rollout percentage is calculated over _all_ devices in the channel, including ones that can't install the bundle.

#### Versioned Channels (Recommended)[¶](#versioned-channels-recommended "Permanent link")

A cleaner pattern: one channel per native version. A bundle uploaded to `production-10` only reaches devices on native version 10, and so on. Every native release ships pinned to its own channel, so bumping the native version automatically opens a fresh channel for compatible web bundles.

For the simplest single-channel case, you can set a fixed channel in `config.xml`:

`[](#%5F%5Fcodelineno-9-1)<preference name="DEFAULT_CHANNEL" value="production" />
`

To pin each native version to its own channel, read the version code at runtime and pass it directly to `sync()` — pass the `channel` to `sync()` rather than calling `setChannel()` separately, so the channel selection is always explicit at the call site:

`` [](#%5F%5Fcodelineno-10-1)const { versionCode } = await cordova.plugins.LiveUpdate.getVersionCode();
[](#%5F%5Fcodelineno-10-2)await cordova.plugins.LiveUpdate.sync({ channel: `production-${versionCode}` });
 ``

The real win is safety. Every deployment is addressed to a specific native version by name, so it's much harder to accidentally ship a bundle to a binary that can't run it. There are no version-range flags to remember on each upload, and no way for a typo to push a bundle to devices outside the intended set. For most teams, channels are the right answer.

### Decision 3: Update Delivery Method[¶](#decision-3-update-delivery-method "Permanent link")

How is the bundle packaged for transport?

#### Zip (Recommended)[¶](#zip-recommended "Permanent link")

The whole web folder is compressed into a single `.zip` and uploaded:

`[](#%5F%5Fcodelineno-11-1)npx @capawesome/cli apps:liveupdates:upload --artifact-type zip
`

The device downloads one compressed file and extracts it. This is the default and almost always the right choice — compression typically halves the size, the download is a single HTTP request, and it works regardless of how your build tool names files.

#### Manifest (Delta Updates)[¶](#manifest-delta-updates "Permanent link")

The alternative is a per-file manifest: each file is uploaded individually, and the device only downloads files whose hash changed since its current bundle.

In theory, this is great for apps that mostly change one or two files between releases. In practice, modern bundlers (Vite, Angular CLI, Webpack 5+) all use _content-hashed filenames_ — `main.7f3a91c2.js`, `chunk-3a7f2b.js`, and so on. The whole point of content hashing is that filenames change whenever content changes, which defeats delta comparison: every file looks new, so every file gets downloaded, and you lose the single-zip compression on top.

The guidance is consistent: start on zip, and only consider manifest if you have specific, stable-named, large static assets you know don't change between builds.

## Security: Code Signing[¶](#security-code-signing "Permanent link")

Live updates run on the same trust model as your native binary: whatever code you ship will execute in your app's context. If someone can replace a bundle in transit — say, on a hostile Wi-Fi network with a forged certificate, or by compromising your delivery server — they get code execution inside your app. Even though the Cloud uses HTTPS, defense in depth matters here.

Code signing closes that gap with two guarantees:

* **Authenticity** — the bundle was produced by someone holding your signing key.
* **Integrity** — the bundle wasn't modified between upload and install.

The mechanism is a standard RSA keypair. You generate a private key and a public key. The private key signs each bundle when you upload it. The public key is embedded in your app config and used to verify the signature of every downloaded bundle before it's applied. If the signature doesn't match, the bundle is rejected.

Generate the keypair with the CLI:

`[](#%5F%5Fcodelineno-12-1)npx @capawesome/cli apps:liveupdates:generatesigningkey
`

This produces `private.pem` and `public.pem`. Add `private.pem` to your `.gitignore`, then sign every upload by passing the private key:

`[](#%5F%5Fcodelineno-13-1)npx @capawesome/cli apps:liveupdates:upload --private-key private.pem
`

Finally, add the public key to your `config.xml` via the `PUBLIC_KEY` preference (the CLI prints it preformatted, with the line breaks stripped, so you can paste it directly):

`[](#%5F%5Fcodelineno-14-1)<preference name="PUBLIC_KEY" value="-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4G...IDAQAB-----END PUBLIC KEY-----" />
`

From that point on, your app verifies every downloaded bundle and refuses to apply anything that isn't signed by your private key. We strongly recommend enabling code signing in production, especially if you operate multiple channels or self-host your bundles. For the full setup including key rotation considerations, see [Code Signing](/docs/cloud/live-updates/code-signing/).

## Best Practices[¶](#best-practices "Permanent link")

Five patterns that consistently separate a smooth live update setup from a painful one.

### Automatic Rollbacks[¶](#automatic-rollbacks "Permanent link")

This is the single most important safety net. The pattern: tell the plugin to roll back to the previous bundle if the new one fails to start, and tell it not to retry a bundle that's already failed once.

Two preferences do the work:

`[](#%5F%5Fcodelineno-15-1)<preference name="READY_TIMEOUT" value="10000" />
[](#%5F%5Fcodelineno-15-2)<preference name="AUTO_BLOCK_ROLLED_BACK_BUNDLES" value="true" />
`

`READY_TIMEOUT` is the deadline: if your app doesn't call `ready()` within this many milliseconds of starting, the plugin assumes the new bundle is broken and reverts to the previous one on the next launch. `AUTO_BLOCK_ROLLED_BACK_BUNDLES` prevents the rollback loop where a broken bundle keeps getting re-downloaded — once a bundle has triggered a rollback, the device blocks it from ever installing again. Note that `AUTO_BLOCK_ROLLED_BACK_BUNDLES` has no effect unless `READY_TIMEOUT` is greater than `0`.

On the app side, call `ready()` as early as you can — right after `deviceready` fires. This is the "I started successfully" signal:

`[](#%5F%5Fcodelineno-16-1)document.addEventListener("deviceready", async () => {
[](#%5F%5Fcodelineno-16-2)  await cordova.plugins.LiveUpdate.ready();
[](#%5F%5Fcodelineno-16-3)});
`

You don't need to wait until your app is fully hydrated. As soon as your bootstrap code has executed without crashing, call `ready()`. The whole mechanism only works if you do.

### Bundle Size Optimization[¶](#bundle-size-optimization "Permanent link")

Bundle size is a user-experience problem: every megabyte is bandwidth on someone's mobile plan and seconds of waiting. It's also a deliverability problem — the bigger the bundle, the more updates fail on flaky connections.

Three optimizations cover most of the wins:

1. **Drop source maps.** They're often the majority of bundle size and they don't belong in production downloads. If you need source maps for error tracking, upload them to your error-tracking service separately — don't ship them to devices.
2. **Stick with `zip`.** Unless you have specific evidence that delta updates will work for your bundle layout (see Decision 3), zip is smaller in practice.
3. **Move heavy static assets out of the bundle.** Hero images, marketing media, downloadable PDFs — anything large that doesn't _have_ to live in the bundle should be served from a CDN and loaded on demand.

Tree-shaking and minification matter too, but most modern build tools handle them by default. Audit with a bundle visualizer if your output is mysteriously large.

### Reasonable Update Checks[¶](#reasonable-update-checks "Permanent link")

A pattern we see surprisingly often:

`[](#%5F%5Fcodelineno-17-1)// Don't do this.
[](#%5F%5Fcodelineno-17-2)setInterval(() => {
[](#%5F%5Fcodelineno-17-3)  cordova.plugins.LiveUpdate.sync();
[](#%5F%5Fcodelineno-17-4)}, 60_000);
`

There's no reason to poll. The built-in `background` strategy already does the right thing — it checks on app start and on resume, with a 15-minute minimum between checks. That's enough freshness for almost every app, and it doesn't burn battery, data, or rate limit.

If you genuinely need to push out an update faster than that, use the **Instant** pattern with a silent push notification. That's the right tool — polling isn't.

### Gradual Rollouts[¶](#gradual-rollouts "Permanent link")

You don't have to release to 100% of users at once. The plugin supports rollouts at the channel level:

`[](#%5F%5Fcodelineno-18-1)npx @capawesome/cli apps:liveupdates:upload --rollout-percentage 10
`

That bundle reaches 10% of devices on the channel. You watch your error tracking, your crash dashboards, your support inbox — and if things look good, you bump it up:

`[](#%5F%5Fcodelineno-19-1)npx @capawesome/cli apps:liveupdates:rollout --channel production --percentage 50
`

As confidence grows, you take it to 100%. If something looks wrong at any point, you stop the rollout before it reaches most of your users.

### Channel Hygiene[¶](#channel-hygiene "Permanent link")

Channels are cheap, but a few habits keep them tidy:

* **Set bundle limits.** Configure a max number of bundles per channel; older ones get garbage-collected automatically.
* **Set expirations on short-lived bundles.** If you're shipping a test bundle for a single QA cycle, use `--expires-in-days` to clean it up automatically.
* **Use protected channels for sensitive deploys.** Protected channels reject any bundle that isn't code-signed.
* **Watch the logs.** The Cloud Console exposes a [logs view](/docs/cloud/live-updates/logs/) showing every device-to-cloud request — invaluable when "why isn't user X getting the update?" turns up in your support inbox.

## End-to-End Example: A Real-World Setup[¶](#end-to-end-example-a-real-world-setup "Permanent link")

Let's put all of this together. The official [Cordova Live Update Demo app](https://github.com/capawesome-team/cordova-live-update-demo) is a small, dependency-free Cordova app that shows the plugin downloading and applying an over-the-air bundle on a real device. You can clone it, point it at your own Capawesome Cloud app, and watch a live update land. The patterns below are the recommended production setup, drawn from that demo.

### config.xml[¶](#configxml "Permanent link")

The plugin is configured through preferences with the app ID, the update strategy, and the automatic rollback settings:

`[](#%5F%5Fcodelineno-20-1)<preference name="APP_ID" value="00000000-0000-0000-0000-000000000000" />
[](#%5F%5Fcodelineno-20-2)<preference name="AUTO_UPDATE_STRATEGY" value="background" />
[](#%5F%5Fcodelineno-20-3)<preference name="AUTO_BLOCK_ROLLED_BACK_BUNDLES" value="true" />
[](#%5F%5Fcodelineno-20-4)<preference name="READY_TIMEOUT" value="10000" />
`

`AUTO_UPDATE_STRATEGY` set to `background` does the heavy lifting. `AUTO_BLOCK_ROLLED_BACK_BUNDLES` and `READY_TIMEOUT` together implement automatic rollback.

### App Initialization[¶](#app-initialization "Permanent link")

At startup, the app waits for `deviceready`, calls `ready()` immediately, and registers a listener for `nextBundleSet` to prompt the user when a new bundle has been staged:

`[](#%5F%5Fcodelineno-21-1)document.addEventListener("deviceready", async () => {
[](#%5F%5Fcodelineno-21-2)  const LiveUpdate = cordova.plugins.LiveUpdate;
[](#%5F%5Fcodelineno-21-3)
[](#%5F%5Fcodelineno-21-4)  await LiveUpdate.ready();
[](#%5F%5Fcodelineno-21-5)
[](#%5F%5Fcodelineno-21-6)  LiveUpdate.addListener("nextBundleSet", async ({ bundleId }) => {
[](#%5F%5Fcodelineno-21-7)    if (!bundleId) {
[](#%5F%5Fcodelineno-21-8)      return;
[](#%5F%5Fcodelineno-21-9)    }
[](#%5F%5Fcodelineno-21-10)    const shouldReload = confirm("A new version is available. Install it now?");
[](#%5F%5Fcodelineno-21-11)    if (shouldReload) {
[](#%5F%5Fcodelineno-21-12)      await LiveUpdate.reload();
[](#%5F%5Fcodelineno-21-13)    }
[](#%5F%5Fcodelineno-21-14)  });
[](#%5F%5Fcodelineno-21-15)});
`

Two things to notice. First, `ready()` is called unconditionally right after `deviceready`, before anything else can fail — this is what makes automatic rollback work. Second, the prompt only fires when `bundleId` is non-null, which happens once the bundle has been fully downloaded and staged. The user never waits for a network call.

### Inspecting the Current State[¶](#inspecting-the-current-state "Permanent link")

The plugin also exposes everything you need to show the current state in your UI — handy for a settings or "about" screen. The demo reads the version name, current bundle, channel, and device ID in one pass:

`[](#%5F%5Fcodelineno-22-1)const [version, bundle, channel, device] = await Promise.all([
[](#%5F%5Fcodelineno-22-2)  LiveUpdate.getVersionName(),
[](#%5F%5Fcodelineno-22-3)  LiveUpdate.getCurrentBundle(),
[](#%5F%5Fcodelineno-22-4)  LiveUpdate.getChannel(),
[](#%5F%5Fcodelineno-22-5)  LiveUpdate.getDeviceId(),
[](#%5F%5Fcodelineno-22-6)]);
`

`getCurrentBundle()` returns a `bundleId` of `null` when the app is running the built-in `www/` bundle, so a simple `bundle.bundleId ?? "Built-in"` tells you at a glance whether a live update is active.

### Shipping an Update[¶](#shipping-an-update "Permanent link")

Once the app is wired up, shipping an update is two commands. Build your web assets, then upload them as a bundle to the channel your devices are listening on:

`[](#%5F%5Fcodelineno-23-1)npm run build
[](#%5F%5Fcodelineno-23-2)npx @capawesome/cli apps:liveupdates:upload --channel production
`

Devices on that channel pick the bundle up on their next launch, download it in the background, and prompt the user to apply it. That's the entire release loop.

## Get Started[¶](#get-started "Permanent link")

You have two paths.

**By hand.** Follow the [Live Updates setup guide](/docs/cloud/live-updates/setup/). Start with `AUTO_UPDATE_STRATEGY` set to `background` plus a `nextBundleSet` listener for the "Always Latest" prompt, versioned channels passed to `sync()`, and the rollback preferences on. That's a production-ready setup in under an hour.

**By example.** Clone the [Cordova Live Update Demo app](https://github.com/capawesome-team/cordova-live-update-demo), point it at your own Capawesome Cloud app, and watch an over-the-air update land on a real device before you wire up anything in your own codebase.

[Try Capawesome Cloud Free](https://capawesome.io)

## Conclusion[¶](#conclusion "Permanent link")

For a Cordova app, over-the-air updates turn a multi-day store cycle into a same-day release. If you take one configuration away from this guide, make it the default stack: the **Always Latest** pattern to keep users informed, **versioned channels** passed to `sync()` so a bundle can never land on an incompatible binary, **zip** delivery for predictable sizes, **automatic rollback** via `READY_TIMEOUT` so a bad bundle heals itself, and **code signing** before you ship to production. Force updates, push-triggered hotfixes, gradual rollouts, and self-hosting are all refinements layered on top of that base.

If you're coming from Appflow, the concepts map almost one to one, and [Migrating from Ionic Appflow to Capawesome Cloud](/blog/migrating-from-ionic-appflow-to-capawesome-cloud/) walks through live updates, native builds, and app store publishing side by side. If you're replacing a deprecated OTA plugin, [Cordova Hot Code Push Alternative for OTA Updates](/blog/cordova-hot-code-push-alternative/) covers the move off `cordova-hot-code-push` and CodePush. For the bigger picture on what else Cordova teams get from Capawesome Cloud, see [Announcing Cordova Support in Capawesome Cloud](/blog/announcing-cordova-support-in-capawesome-cloud/).

Questions, feedback, or war stories from your own live update rollout? Drop into the [Capawesome Discord server](https://discord.gg/VCXxSVjefW) — that's where we're always happy to chat. And if you'd like the next deep-dive in your inbox, subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/).

June 9, 2026 

 Back to top 