Native Update โ a project by Ahsan Mahmood
Over-the-air updates for Capacitor apps โ bundle signing, staged rollout, and automatic rollback when a bundle fails to boot twice.
How long does a typo live in a shipped mobile app?
As long as the review queue takes, plus however long your users take to update, and neither of those numbers belongs to you. Meanwhile the broken screen keeps opening.
That is the problem an over-the-air channel exists to solve, and it brings a worse one with it.
A path that can replace an app's code remotely is a path somebody else would like to use.
So integrity comes first. Every bundle is signed and verified with SHA-256 and RSA-SHA256, and one that does not verify does not run.
The second requirement is the one people skip. A bad bundle has to be able to undo itself with nobody in the loop, because the person who could intervene is asleep in a different timezone.
Fail to boot twice and the native side rolls back on its own.
native-update is a Capacitor plugin on npm, and around it sits everything an update channel actually needs rather than the plugin on its own.
A release-management CLI handles bundles. It creates them, signs them and verifies them before any of them leaves the machine.
A Laravel 11 and Nova 5 backend holds apps, keys and releases behind bearer-token authentication and role-based access control, so who may publish to a channel is a question with an answer rather than a convention. A React 19 dashboard sits on top of that, itself wrapped with Capacitor for the Play Store.
Store update checks and throttle-aware review prompts live in the same plugin, because an app that needs an over-the-air path also needs to know when a real store update is waiting for it.
Documentation runs to 58 public pages, with two runnable example apps beside it, because a plugin that changes how an app boots cannot honestly be explained in a README.
That is the difference between a package you can adopt and a package you can read about.
198 tests hold it: 62 in TypeScript and 136 in Pest, with a Detox suite running the end-to-end path against a real build rather than against a mock of one.
One release deliberately removed Firestore so the whole system runs through a single HTTP backend. Deleting a dependency is the change that never demonstrates well and always pays.
The plugin makes one demand.
It must confirm a healthy boot. An app that forgets rolls itself back after two cold starts and looks, from the outside, exactly like an update that was broken โ which is the most expensive kind of bug, because it points at the wrong thing.
Signing keys are the other cost. A key you lose is a channel you can no longer publish to, and there is no support desk here to reissue one for you.
This is one of the few things here with a backend that has to stay awake, which is the shape of the problem rather than a slip: an update channel needs somewhere for the update to come from.
Everything else I build is arranged so that no server has to be. This one could not be, and that is the honest price of a release path that does not queue behind a store reviewer.
There is no iOS build here. There is no Apple Developer account behind any of this, so Android is where the update path has actually been exercised.
A piece of infrastructure nobody can inspect is a piece of infrastructure nobody should install. The npm page, the 58 pages of documentation and the two runnable example apps are all open to anybody, and that is the whole of the argument.
What it does, and what that costs to build
- Live/OTA bundle updates (no store re-submission)
- App-store update checks (Play Core + StoreKit)
- In-app review prompts (throttle-aware)
- Signed bundles + crash-rollback safety
- Release-management CLI (bundle create/sign/verify)
- Laravel 11 + Nova 5 SaaS backend (Sanctum + RBAC)
- Public 58-page Docusaurus docs site
Built with
- TypeScript
- CapacitorJS
- Kotlin
- Swift
- Node.js
- Laravel 11
- Laravel Nova
- React 19
Worth knowing
- npm
- package
- capacitor
- ota-updates
- laravel
- plugin
Who built native-update?
Ahsan Mahmood built it alone: the plugin, the release CLI, the Laravel backend, the dashboard and the documentation site. It is published on npm as native-update and the dashboard is at nativeupdate.aoneahsan.com. The native Android implementation and the TypeScript surface were written by the same person, which is why they agree with each other.
What happens if an over-the-air bundle breaks the app?
The native side rolls back on its own after the app fails to boot twice, so a broken bundle recovers without anybody being woken up. That is why the plugin requires the app to confirm a healthy start rather than assuming one, because silence is treated as failure. Bundle integrity is checked before that point too, with SHA-256 and RSA-SHA256 signing, so a bundle that has been altered never gets far enough to crash.
Does native-update work on iOS apps?
There is no iOS build and no App Store presence behind this work, because there is no Apple Developer account here โ so the honest answer is that the over-the-air path is proven on Android. Treat anything else as unverified until somebody with an Apple account has actually run it.
Where do the bundles come from?
From a Laravel 11 and Nova 5 backend that holds apps, keys and releases, with bearer-token authentication and role-based access control in front of them. A release-management CLI creates, signs and verifies a bundle before it is uploaded, so the signing step is part of the release rather than an optional extra. One release deliberately removed Firestore from the system so that everything runs through that single HTTP backend.
How do I add native-update to a Capacitor app?
Install it from npm as native-update, then wire the healthy-boot confirmation into your app's start-up path before anything else. The documentation site runs to 58 pages and two runnable example apps ship with the project, which between them cover the wiring the README cannot. Skipping the boot confirmation is the one mistake that turns a working install into an app that rolls itself back.
https://aoneahsan.com/projects/com.aoneahsan.nativeupdate