Back to Home

Manual update of Capacitor bundles without stores

The article describes manual integration of @capgo/capacitor-updater for OTA updates of JS bundles in Capacitor apps. Code examples of the provider are provided, preparation of archives via CLI, checking compliance with store rules. Suitable for middle/senior developers.

Capacitor: bundle updates bypassing store approvals
Advertisement 728x90

# Manual Capacitor Bundle Updates Without App Stores

Mobile Capacitor apps require a full release to the stores even for minor JS code or config tweaks. Without native code changes, approval in Google Play, RuStore, AppGallery, and App Store takes days. Automation helps, but moderation is inevitable.

The solution is updating the contents of the dist (or build) directory without rebuilding the native part. The @capgo/capacitor-updater plugin allows over-the-air bundle replacement in manual or automatic mode.

Compliance with Store Policies

JS bundle updates are allowed by all platforms if conditions are met:

Google AdInline article slot
  • Google Play: Interpreted code (JS) is allowed as long as it doesn't violate resource abuse policies.
  • App Store: Downloaded code doesn't change the app's purpose, doesn't create a code store, and doesn't bypass WebView security.

These conditions are automatically met for Capacitor hybrid apps.

Installation and Setup

Install dependencies:

npm i @capgo/capacitor-updater compare-versions

In capacitor.config.ts add:

Google AdInline article slot
plugins: {
  CapacitorUpdater: {
    autoUpdate: false,
  },
},

The autoUpdate: false mode enables manual update management.

Create an AppUpdateProvider for React (adapt to your framework):

const AppUpdateProvider: FC<PropsWithChildren> = ({ children }) => {
  return children;
};

export default AppUpdateProvider;

Update Logic

In the provider, call CapacitorUpdater.notifyAppReady() within the first 10 seconds of launch — otherwise, an error occurs.

Google AdInline article slot

Fetch the latest release data via API (GitHub or internal service). Compare versions using compare-versions.

Example of a full useEffect:

useEffect(() => {
  if (appRelease === null) {
    dispatch(getAppRelease({ type: ReleaseType.APP, last: true }));
  } else {
    const downloadBundle = async () => {
      dispatch(setAppUpdating(true));
      return CapacitorUpdater.download({
        url: APP_BUNDLE_DOWNLOAD_LINK,
        version: appRelease.version,
      });
    };

    CapacitorUpdater.addListener('downloadComplete', async ({ bundle: downloadedBundle }) => {
      try {
        await CapacitorUpdater.set(downloadedBundle);
        dispatch(setAppUpdating(false));
      } catch (e) {
        console.error(e);
        dispatch(setAppUpdating(false));
      }
    });

    if (compare(String(version), appRelease.version, '<')) downloadBundle();

    return () => {
      CapacitorUpdater.removeAllListeners();
      App.removeAllListeners();
    };
  }
}, [appRelease]);

Preparing the Bundle

Create the bundle archive strictly using Capgo CLI:

npx @capgo/cli bundle zip [appId] --name [myapp]
  • appId from capacitor.config.ts.
  • Update version in package.json before building.
  • Upload the ZIP to an HTTPS server (GitHub Releases or your own hosting).

The archive link is passed to CapacitorUpdater.download({ url, version }).

After download, downloadComplete triggers CapacitorUpdater.set() — the bundle is replaced, and the app restarts.

App Integration

Full provider:

const AppUpdateProvider: FC<PropsWithChildren> = ({ children }) => {
  CapacitorUpdater.notifyAppReady();
  const dispatch = useAppDispatch();
  const appRelease = useAppSelector(selectAppRelease);

  // useEffect how vyshe

  return children;
};

In main.tsx wrap App:

root.render(
  <Provider store={store({})}>
    <AppUpdateProvider>
      <App />
    </AppUpdateProvider>
  </Provider>
);

Key Points

  • Updates work only for JS bundles, without native plugins.
  • Bundle is archived via npx @capgo/cli bundle zip — other methods are incorrect.
  • Calling notifyAppReady() is mandatory within the first 10 seconds of launch.
  • Version comparison: compare(currentVersion, newVersion, '<').
  • Self-hosted Capgo — an alternative for full control.

— Editorial Team

Advertisement 728x90

Read Next