Back to Home

OTA updates Capacitor: architecture and GitHub

The article describes the refactoring of the OTA update system for Capacitor applications using GitHub Releases and clean architecture. NATIVE/PATCH strategies, adapters for Capgo, Zustand store and download progress handling are considered.

Clean OTA architecture for Capacitor apps
Advertisement 728x90

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:

Google AdInline article slot
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.

Google AdInline article slot

Structure:

  • features/app-update/model/types.ts — statuses (IDLE, CHECKING, AVAILABLE) and strategies (NONE, NATIVE, PATCH)
  • features/app-update/model/contracts.ts — dependency interfaces
  • shared/api/app-updater/capgo-updater-runtime.ts — wrapper over the plugin
  • shared/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.0 vs 1.0.2NATIVE (store)
  • 1.0.3 vs 1.0.2PATCH (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:

Google AdInline article slot
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

Advertisement 728x90

Read Next