# 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 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:
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.
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]
appIdfromcapacitor.config.ts.- Update
versioninpackage.jsonbefore 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
No comments yet.