React Native OTA Updates (Beta)
React Native OTA updates let an installed host app load newer Metro Module Federation remote bundles without releasing a new binary through the App Store or Play Store. Zephyr supports this flow with two packages:
zephyr-metro-pluginpublishes Metro Module Federation bundles to Zephyr Cloud and resolves remote manifests foriosandandroidbuilds.zephyr-native-cacheruns in the host app, verifies remote bundle hashes, stores valid bundles on device, polls remote manifests for changes, and exposes APIs to apply downloaded updates.
React Native OTA support for Metro is currently in beta. APIs and recommended wiring may change while the native cache and Metro integration continue to stabilize.
OTA updates only update JavaScript bundles loaded through Metro Module Federation. Native code changes, new native modules, native dependency linking changes, permission changes, and host-shell changes still require a normal app-store release.
For a complete working reference, see the zephyr-native-cache-test example. The example includes a host app, two remotes, local OTA fixtures, Zephyr-backed OTA scripts, update prompts, cache status UI, and rollback coverage.
How It Works
- You publish a Metro Module Federation remote with
zephyr-metro-plugin. - Zephyr Cloud creates an immutable version with platform-specific artifacts and an
mf-manifest.json. - Your released host app resolves a remote manifest through a Zephyr dependency such as
zephyr:profile@production. zephyr-native-cachereads bundle hashes from the manifest through its Module Federation runtime plugin.- When a remote is loaded, the cache layer downloads the bundle natively, verifies its SHA-256 hash, caches it on device, and evaluates it.
- Background polling or a manual update check fetches known manifests again. If hashes changed, the cache layer pre-downloads the new bundles.
- Your app decides when to apply the update by reloading the React Native JavaScript context.
Rollbacks use the same mechanism. If you roll back a Zephyr environment or move a tag back to an older remote version, manifest polling sees the older bundle hash as the active target, downloads it, and the host can apply it on the next reload.
Install Packages
Install the Metro and Module Federation packages in every host and remote app:
Install the native cache package in the host app:
For iOS, run CocoaPods after adding zephyr-native-cache:
Configure The Host
Add the native cache runtime plugin to the host's Module Federation config. The plugin extracts bundle hashes from remote manifests and registers the manifest URLs that polling will check later.
Use local HTTP manifest URLs during local development. Use Zephyr selectors such as zephyr:profile@production, zephyr:profile@staging, or zephyr:profile@latest for builds that should resolve through Zephyr Cloud and receive OTA changes from the corresponding environment or tag.
You can also declare the same dependency in the host package.json so Zephyr dependency relationships are visible in the dashboard:
For more dependency selector options, see Remote Dependencies.
Register The Cache At Startup
Register the native cache before any remote bundle can load. In a Metro Module Federation host, this usually means registering it in index.js before AppRegistry.registerComponent.
Production builds enable the cache automatically. Development builds keep the cache disabled by default so normal Metro development behavior is preserved. Set forceCacheInDev: true only when you intentionally want to debug OTA behavior against Metro dev servers.
Configure Remote Builds
Use the Zephyr RNEF plugin so rnef bundle-mf-remote and rnef bundle-mf-host upload Metro artifacts to Zephyr after the Module Federation build completes.
Then publish a remote for each platform you support:
The host must resolve the same platform that the remote was published for. withZephyr({ target: 'ios' }) resolves iOS remote artifacts, and withZephyr({ target: 'android' }) resolves Android remote artifacts.
Apply Updates In The App
The cache layer intentionally does not force a restart UX. It provides state and controls so your app can choose whether to apply updates silently, show a banner, wait for a safe navigation point, or expose controls in a settings screen.
Use useCacheStatus to drive an update prompt:
Use manual checks when you want a pull-to-refresh, settings, or internal QA flow:
Use downloadAndApply only when an immediate JavaScript reload is acceptable:
downloadAndApply downloads updated bundles and triggers the native React Native reload path. Persist any critical UI state before using it.
Runtime Behavior
When a remote bundle loads, the cache layer returns one of these statuses:
The cache skips safely when a bundle has no manifest hash, a hash verification fails, a native file operation fails, or the native module is unavailable. Hash mismatches are not cached.
The default cache limits are:
The cache evicts stale bundles with an LRU policy on cold start. Fresh bundles are preserved even if the cache is temporarily over the size limit.
Deploy And Roll Back
To deploy an OTA update:
- Publish a new remote version for
iosandandroid. - Move the Zephyr environment or tag that the host depends on to the new version.
- Wait for polling or call
ZephyrNativeCache.checkForUpdates(). - Prompt the user or call
ZephyrNativeCache.reloadApp()when your app is ready to apply the downloaded update.
To roll back, use the Zephyr dashboard to roll the remote environment back or move the tag back to a previous version. The next update check downloads the rolled-back bundle and applies it through the same reload flow.
Example Repository
Use zephyr-native-cache-test as the implementation reference. Start with these files:
apps/host/index.jsfor cache registration.apps/host/metro.config.jsfor host runtime plugin wiring.apps/host/rnef.config.mjsfor RNEF command integration.apps/host/src/components/DevToolsPanel.tsxfor polling status and manual controls.ZEPHYR_OTA_DEMO.mdfor the Zephyr-backed OTA dashboard walkthrough.