Replace each Appflow feature the project uses - Live Updates, native cloud builds, store deployment - and remove Appflow only after each replacement is verified.
Context: Ionic stopped selling Appflow in February 2025; existing customers keep access until December 31, 2027 with no new features. Plan the migration well before that date.
When to Use
TRIGGER when:
package.jsonhas@capacitor/live-updatesorcordova-plugin-ionic.capacitor.config.*hasplugins.LiveUpdates, orconfig.xml/Info.plist/strings.xmlhold Appflow deploy keys (APP_ID,CHANNEL_NAME,UPDATE_METHOD,IonAppId,IonChannelName).- CI calls
ionic deploy,appflow,ionic package build, or the Appflow dashboard triggers builds and store uploads.
Do not use for:
@ionic-enterprise/*plugins ->ionic-enterprise-sdk-migration.- Capawesome Cloud ->
capawesome-live-update-migration. - Greenfield Capgo setup ->
capgo-live-updates.
Live Project Snapshot
Detected Appflow-related packages and scripts:
!node -e "const fs=require('fs');if(!fs.existsSync('package.json'))process.exit(0);const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));const out=[];for(const section of ['dependencies','devDependencies']){for(const [name,version] of Object.entries(pkg[section]||{})){if(name==='@capacitor/live-updates'||name==='cordova-plugin-ionic'||name.includes('appflow')||name==='@ionic/cli'||name==='@capgo/capacitor-updater'||name==='@capacitor/core')out.push(section+'.'+name+'='+version)}}for(const [name,cmd] of Object.entries(pkg.scripts||{})){if(/appflow|ionic deploy|ionic package|live-updates/i.test(cmd))out.push('scripts.'+name+'='+cmd)}console.log(out.join('\n'))"
Possible Appflow config and workflow paths:
!find . -maxdepth 4 -not -path '*/node_modules/*' \( -name '.io-config.json' -o -name 'ionic.config.json' -o -name 'config.xml' -o -name 'capacitor.config.json' -o -name 'capacitor.config.ts' -o -name 'capacitor.config.js' -o -path './.github/workflows/*' -o -name '.gitlab-ci.yml' -o -name 'appflow.config.json' \)
Procedure
Step 1: Inventory (report before editing)
rg -n "@capacitor/live-updates|cordova-plugin-ionic|LiveUpdates|IonAppId|IonChannelName|UPDATE_METHOD|CHANNEL_NAME|ionic deploy|appflow|dashboard.ionicframework.com|ionic.io/appflow" \
-g '!node_modules' .
Record per feature:
- Live Updates: SDK in use, Appflow app ID, channels and which binaries point at them, update method (
background,none, always-latest on resume, force update),maxVersions, any code callingsync(),download(),extract(),reload(). - Native builds: build stack, signing certificates/profiles and keystores stored in Appflow, environment variables, native config overrides.
- Store deploys: TestFlight / Play destinations and tracks.
Ask the user to export signing credentials from Appflow (or confirm they have originals) before deleting anything there.
Step 2: Live Updates -> Capgo
npm uninstall @capacitor/live-updates # or: npm uninstall cordova-plugin-ionic
npm install @capgo/capacitor-updater
npx cap sync
For cordova-plugin-ionic, also remove its <plugin>/<variable> entries from config.xml and any Ion* keys from native files.
Replace the config block:
// capacitor.config.ts
plugins: {
// remove LiveUpdates: { appId, channel, autoUpdateMethod, maxVersions }
CapacitorUpdater: {
autoUpdate: true,
autoDeletePrevious: true,
},
},
| Appflow | Capgo |
|---|---|
appId |
Capgo app created with npx @capgo/cli@latest init (uses appId from capacitor.config) |
channel |
Capgo channel (dashboard/CLI); set default channel server-side, or defaultChannel in config for per-build overrides |
autoUpdateMethod: 'background' |
autoUpdate: true (default behaviour: download in background, apply on next start) |
autoUpdateMethod: 'none' + manual sync() |
Keep autoUpdate: true unless the product needs custom timing; otherwise autoUpdate: false + manual API |
maxVersions |
autoDeletePrevious: true plus server-side bundle retention |
| Binary-version targeting | Channel per native major, or --min-update-version / metadata strategy (capgo-release-workflows) |
| Force update | Mark bundle/channel behaviour in Capgo and listen for majorAvailable or use directUpdate |
Add the one required hook once the app shell renders:
import { CapacitorUpdater } from '@capgo/capacitor-updater';
void CapacitorUpdater.notifyAppReady();
Without it Capgo treats the bundle as broken and rolls back.
API mapping (keep manual calls only if the product needs custom UI or timing):
Appflow (@capacitor/live-updates) |
Capgo |
|---|---|
LiveUpdates.sync() |
Not needed with autoUpdate: true; manual: CapacitorUpdater.getLatest() + download() |
LiveUpdates.reload() |
CapacitorUpdater.set({ id }) (applies and reloads) or reload() |
resume listener calling sync() |
Remove; auto-update checks on resume |
SplashScreen.hide() after sync |
Plain SplashScreen.hide() after app boot; optional appReady listener |
Upload the first bundle to a test channel:
npm run build
npx @capgo/cli@latest bundle upload --channel development
Step 3: Native cloud builds
Two replacements; ask which the user prefers:
- Capgo Cloud Build:
npx @capgo/cli@latest build credentials save --platform ios|android ..., thennpx @capgo/cli@latest build request <appId> --platform ios|android. Details incapgo-native-builds. - Repo-owned CI (GitHub Actions, GitLab, Fastlane):
capacitor-ci-cd.
Carry over environment variables, build-time config, and signing inputs exactly; change one thing at a time.
Step 4: Store publishing
Capgo Cloud Build can upload to TestFlight / Play when store credentials are saved; otherwise use CI upload steps (capacitor-ci-cd) or manual submission (capacitor-app-store). Keep bundle IDs, Play tracks, and credentials unchanged.
Step 5: Cut over and clean up
- Ship a native build containing
@capgo/capacitor-updaterto the stores. Devices on older binaries keep pulling from Appflow until they update - keep Appflow channels live until adoption is high enough. - Publish web updates to both systems during the overlap, or freeze Appflow at the last bundle shipped with the new binary.
- Remove Appflow packages,
plugins.LiveUpdates,.io-config.jsonreferences, Appflow CI jobs, and secrets once no supported binary depends on them.
Verification
rg -n "@capacitor/live-updates|cordova-plugin-ionic|LiveUpdates\\." -g '!node_modules' .returns nothing (after cleanup).- Fresh install of the new binary: logs show
notifyAppReadycalled; Capgo dashboard shows the device. - Upload a test bundle to a non-production channel; device downloads and applies it on next start.
- Upload a deliberately broken bundle (no
notifyAppReady) to the test channel; device rolls back. - New native build pipeline produces signed IPA/AAB with the same bundle ID and a higher build number.
Error Handling
| Problem | Fix |
|---|---|
| App rolls back every update | notifyAppReady() not reached (called too late, behind login, or code path throws); call it right after the shell renders |
Cannot find API key in local folder or global, please login first from the Capgo CLI |
npx @capgo/cli@latest login <apikey> locally, or CAPGO_TOKEN in CI |
| Updates never arrive | Device on a channel without the bundle, or bundle incompatible with native version; check channel and npx @capgo/cli@latest bundle compatibility |
| Old Appflow update overwrote Capgo bundle | Old SDK still installed in the binary; confirm @capacitor/live-updates / cordova-plugin-ionic removed and npx cap sync run before building |
| Build fails after removing Appflow | Appflow injected env vars or native config; recreate them in CI or Capgo Cloud Build environment |