+1 (415) 843-4662

Migrating Live Updates off Ionic Appflow: a step-by-step guide

Ionic Appflow stays available to existing customers only until December 31, 2027, and new sales already ended in February 2025. If your app pulls over-the-air web updates from Appflow (the feature that started life as Ionic Deploy), this is the migration we run most often in 2026. It is not hard, but it has a few places where a wrong move leaves users on a stale or blank bundle with no way to push a fix. This guide is the process we use.

1. Inventory what Appflow is doing for you

Before touching code, write down the current state. In the Appflow dashboard, for each app record:

  • Channels (for example Production, Staging, QA) and which native binaries are bound to which channel
  • Update strategy: background, always-latest, or none with manual sync() calls in code
  • Minimum native version constraints and any channel-level rollout percentages
  • Who holds the Appflow API tokens used by CI

In the codebase, find the integration points:

grep -rn "cordova-plugin-ionic\|@ionic/appflow\|Deploy\." src/ capacitor.config.*

Typical hits are capacitor.config.ts (a plugins.LiveUpdates or cordova.preferences block with APP_ID, CHANNEL_NAME, UPDATE_METHOD), a service that calls Deploy.sync() or Deploy.checkForUpdate(), and a CI step that calls ionic deploy build. Each one gets a replacement in the steps below.

2. Choose a provider

Two Capacitor-native providers cover almost every migration we do:

  • Capgo — open-source updater plugin (@capgo/capacitor-updater), cloud or self-hosted backend, channels, per-device targeting, end-to-end encryption of bundles. Good fit for teams that want a self-hosting option or very granular rollouts.
  • Capawesome Cloud@capawesome/capacitor-live-update plugin from the team behind a large Capacitor plugin catalogue, with channels, artifact signing, and a straightforward CLI.

Both support rollback, both have free tiers for evaluation, and both publish migration notes for Appflow users. The decision usually comes down to self-hosting and data-residency requirements. Pick one; do not run both in production.

3. Install the new updater alongside the old one

Keep the Appflow plugin installed for now. The goal is a build that can talk to either backend so you can switch with a flag rather than an emergency release.

With Capgo (Capacitor 8):

npm install @capgo/capacitor-updater@latest
npx cap sync

In your app bootstrap, the one call that matters is notifyAppReady(). If it is not called shortly after launch, the plugin assumes the new bundle crashed and rolls back to the previous one — which is the behaviour that makes live updates safe, and also the first thing teams forget:

import { CapacitorUpdater } from '@capgo/capacitor-updater';

export async function initLiveUpdates() {
  // Tell the updater this bundle booted successfully; otherwise it rolls back.
  await CapacitorUpdater.notifyAppReady();
}

Call it from your root component's initialisation (Angular APP_INITIALIZER or the root ngOnInit, React's top-level useEffect, Vue's onMounted).

With Capawesome the shape is the same:

npm install @capawesome/capacitor-live-update
npx cap sync
import { LiveUpdate } from '@capawesome/capacitor-live-update';

export async function initLiveUpdates() {
  await LiveUpdate.ready();
}

Put the provider choice behind a single function so the rest of the app does not care which plugin is active.

4. Configure channels to mirror Appflow

Recreate your Appflow channels one-for-one in the new provider before you change anything in the app. A native binary should map to exactly one channel, and the channel name should be baked into the build, not chosen at runtime from a remote config you might lose access to.

With Capgo, the CLI initialises the app and its channels:

npx @capgo/cli@latest init YOUR_API_KEY
npx @capgo/cli@latest channel add production
npx @capgo/cli@latest channel add staging

Set the default channel in capacitor.config.ts:

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

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'Example',
  webDir: 'www',
  plugins: {
    CapacitorUpdater: {
      defaultChannel: process.env.LIVE_UPDATE_CHANNEL ?? 'production',
    },
  },
};

export default config;

With Capawesome, versioned channels let you tie the channel to the Android versionCode at build time (a string resource capawesome_live_update_default_channel in build.gradle), which is the cleanest way to guarantee a web bundle never reaches an incompatible native version.

5. Keys and signed bundles

Appflow handled bundle integrity for you. In your own pipeline, treat the signing key like a release certificate:

  • Generate the key once, store the private half in your CI secret store, never in the repository
  • Sign every bundle in CI; configure the app to reject unsigned bundles
  • Rotate the key on the same schedule as your other release credentials

With Capgo, npx @capgo/cli key create generates the key pair and bundle upload encrypts bundles end-to-end when the key is present, so a compromised CDN cannot serve a tampered bundle. Capawesome Cloud supports artifact signing on upload. Check each provider's current docs for the exact flags — they change more often than the concept does.

6. Replace the CI step

Your CI currently ends with something like ionic deploy build --channel Production. Replace it with the new provider's upload, wired to the same trigger:

# .github/workflows/live-update.yml
name: live-update
on:
  push:
    branches: [main]
jobs:
  upload:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npm run build
      - run: npx @capgo/cli@latest bundle upload --channel production --apikey "$CAPGO_TOKEN"
        env:
          CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}

Keep the Appflow step in the workflow but behind a condition for now; you will delete it at the end.

7. Staged rollout

Ship a native release that contains the new updater and the flag that selects it. Then:

  1. Point the staging channel at the new provider and push a trivial web change. Confirm devices on the staging binary receive it, and confirm that killing the app mid-update rolls back cleanly.
  2. Enable the new provider for a small percentage of production devices (both providers support percentage rollouts per channel).
  3. Watch crash reports and the provider's update stats for one full release cycle.
  4. Move to 100%. From this point Appflow is no longer serving updates, but the plugin is still installed as a safety net.

8. Decommission checklist

Only after a native release has gone through the stores without the Appflow plugin:

  • Remove cordova-plugin-ionic / @ionic/appflow from package.json and run npx cap sync
  • Delete the LiveUpdates / cordova.preferences block from capacitor.config.ts
  • Delete the conditional Appflow step from CI and revoke the Appflow API tokens
  • Export build logs or artifacts you are required to retain, because the Appflow dashboard will not be there in 2028
  • Record the provider, channel mapping, and key-rotation owner in your runbook

What usually goes wrong

  • Forgetting notifyAppReady() — every update appears to "fail" and rolls back. Put it in the first render path, not behind a login.
  • Channel drift — a QA binary pointed at production. Bake the channel into the build.
  • Native changes in a web update — live updates only carry web assets. A new Capacitor plugin still needs a store release; gate it with a minimum native version.

If you would rather have someone who has done this a dozen times run it, our Appflow migration service starts with a free audit of your current setup.