Capacitor OTA Updates Architecture: Strategies and GitHub Releases
Capacitor enables updating the JavaScript bundle of a mobile app without requiring review on Google Play or the App Store. The native shell remains unchanged, while the product code is replaced via a ZIP archive. The system uses GitHub Releases as the version source and @capgo/capacitor-updater for downloading. Automatic rollback on failures is ensured by notifyAppReady() within the first 10 seconds of launch.
Legality and Limitations
Google Play allows updates to interpreted code (JavaScript) as long as the app's primary purpose isn't altered. Apple permits downloading JS bundles provided functionality is preserved and security mechanisms aren't bypassed. The line is clear: changes to android/ or ios/ require publishing through the store.
Installation:
npm install @capgo/capacitor-updater
npx cap sync
In capacitor.config.ts, disable auto-update:
const config: CapacitorConfig = {
plugins: {
CapacitorUpdater: {
autoUpdate: false,
},
},
};
Build the bundle:
npm run build:dist
npx @capgo/cli bundle zip io.your.app --name app-bundle.zip
New Architecture: FSD and Ports & Adapters
The code is split into src/features/app-update/ (business logic) and src/shared/api/ (adapters). The feature operates through interfaces, unaware of Capgo or GitHub. A Zustand store manages state, with UI components isolated.
Structure:
features/app-update/model/types.ts— statuses (IDLE,CHECKING,AVAILABLE) and strategies (NONE,NATIVE,PATCH)features/app-update/model/contracts.ts— dependency interfacesshared/api/app-updater/capgo-updater-runtime.ts— wrapper over the pluginshared/api/github/github-runtime.ts— requests to GitHub API
Update Strategies by Semver
Determining update type:
const isReleaseHasNativeUpdate = (current: SemVer, remote: SemVer): boolean =>
remote.major > current.major || remote.minor > current.minor;
const isReleaseHasPatchUpdate = (current: SemVer, remote: SemVer): boolean =>
remote.major === current.major &&
remote.minor === current.minor &&
remote.patch > current.patch;
1.1.0vs1.0.2→NATIVE(store)1.0.3vs1.0.2→PATCH(OTA)
GitHub Releases as a Single Source
GitHub API returns the tag, changelog, and link to app-bundle.zip. The use-case parses versions and selects the asset:
export const checkForAvailableUpdate = async (deps: AppUpdateDependencies): Promise<CheckResult> => {
const releaseResult = await deps.githubRuntime.getLatestReleaseInfo();
// ...
const bundleAsset = assets.find(a => a.name === 'app-bundle.zip');
// ...
};
Adapter for GitHub:
export const createGithubRuntime = (): GithubRuntime => ({
getLatestReleaseInfo: async () => {
const response = await fetch(config.APP_RELEASE_URL);
const data: GithubLatestReleaseResponse = await response.json();
return { status: true, data };
},
});
Capgo Adapter and Event Handling
The interface isolates business logic from the plugin:
export const createCapgoUpdaterRuntime = (): CapgoUpdaterRuntime => ({
downloadBundle: (url, version) => CapacitorUpdater.download({ url, version }),
notifyAppReady: () => CapacitorUpdater.notifyAppReady(),
addDownloadListener: (listener) => CapacitorUpdater.addListener('download', listener),
});
The Zustand store subscribes to progress:
startDownloadUpdate: async () => {
set({ status: APP_UPDATE_STATUSES.DOWNLOADING, downloadProgress: 0 });
await capgoRuntime.addDownloadListener(({ percent }) => {
set({ downloadProgress: percent });
});
const bundle = await capgoRuntime.downloadBundle(bundleDownloadUrl, remoteVersion);
},
Application: CapacitorUpdater.set(bundle) + App.minimizeApp() for a smooth restart.
Initialization and UI
In main.tsx after rendering:
await initializeUpdater();
initializeUpdater() creates dependencies, calls notifyAppReady(), and checks for updates. The AppUpdateInfo component displays conditionally based on status (AVAILABLE, DOWNLOADING, DOWNLOADED).
Key Points:
- OTA only for patch versions; major/minor updates go through the store
notifyAppReady()within the first 10s prevents rollback- GitHub Releases combines versioning, CDN, and API in one place
- Ports/adapters architecture simplifies testing and provider replacement
- Download progress via native plugin events
— Editorial Team
No comments yet.