Hot Updater: Over-the-Air Patch Delivery

Microsoft recently ended support for their all-in-inclusive React Native over-the-air (OTA) patch deployment solution called CodePush. The Apple and Google Play Store review processes are slow, and it often takes time for users to adopt updates. Having CodePush available over the past several years was a game changer—it allowed us to deliver hotfixes and features instantly, bypassing the review process entirely. After some research, we discovered a Node.js module called @hot-updater, which turned out to be exactly what we needed. It provides boilerplate bundle management, lets us store patch data in any system we choose, and removes the risk of our deployment pipeline being revoked by a third party.


What Is Hot Updater?

Hot Updater is a Node.js module that helps you “roll your own” patch deployment system for React Native apps. It’s a development dependency that provides the boilerplate client-side code needed to create, store, and load patch bundles. Once added to your project locally, you’ll need to set up a config by supplying a Storage Plugin and a Database Plugin. You can use the built-in plugins provided, or create your own custom versions tailored to your specific storage solution.

Patches are downloaded in real time to each device and installed over the initial bundle—or the last applied patch. If the patch is marked with force update, it will interrupt the user and install immediately. Otherwise, the update will be deferred until a less intrusive moment.

Keep in mind: this includes your assets folder as well, so any updated static content will be delivered as part of the patch.

Quick References:


Getting Started

Add the Package to your React Native Project

Use one of the following package managers to install @hot-updater:

 npm install hot-updater --save-dev
 pnpm add hot-updater -D


Implement the config

In your project’s root directory make sure there is a file called hot-updater.config.ts (if not create one.) At this point you will need to provide the Storage Plugin and the Database Plugin. Look at the ones they provide and see if anyone of them would work for you. Each solution has it’s own guide!


./hot-updater.config.ts


The Storage Plugin

The Storage Plugin is the simpler of the two to implement. It takes a locally generated bundle and uploads it to a location accessible via the public internet—or removes it from that location when needed. Hot Updater currently provides several pre-made Storage Plugin options. If any of the following solutions fit your needs, just click the link and follow the setup instructions!


The default S3 plugin provided by Hot Updater stores all bundle data at the root of the bucket. To better organize our deployments, I created a custom S3 plugin—using theirs as a starting point—that stores bundles in structured, categorical folders (e.g., grouped by environment or patch version). My Key path looks something like this:

for example:

If you prefer to write your own Storage Plugin, you must implement the following interface type structure:


Storage Plugin Implementation Breakdown

name
Provide a unique name for your plugin. This is for identification only and doesn’t affect functionality.

uploadBundle
This function receives a bundleId and the local file path (bundlePath) to the generated bundle. Use this to determine where and how the bundle should be stored. Typically, you would name the file after the bundleId to make it easier to identify in your storage system.

In my solution, I designed a custom path structure that includes environment labels like "beta" or “release”, along with an incremental patch number embedded in the S3 key path. This makes it easier to separate environments and track bundle history.

deleteBundle
This is the inverse of uploadBundle. It should remove the bundle and any related assets from the location where it was uploaded.


The Database Plugin

The Database Plugin manages metadata about deployed bundles—tracking what’s available and where it applies. Again, check out the provided plugins to see if any of them meet your needs (see links above).

In my case, the provided S3 plugin didn’t support the custom key path structure I established in the Storage Plugin, so I used their implementation as a reference to build my own. This allowed me to organize bundles using environment-specific paths (e.g., by "beta" or "release" folders). I also introduced support for environment variables—such as MODE_KEY—to limit the scope of data returned. For example, running the console with a MODE_KEY value lets me filter which bundles are shown, making it easier to work within a specific deployment context.

If you choose to write your own Database Plugin, it works by creating a local state—a snapshot of the currently available bundles. You can make changes to this local state and then “commit” the updated structure back to your data solution.

A custom Database Plugin must implement the following interface structure:


Database Plugin Implementation Breakdown

name
Provide a unique name for your plugin. This is used for identification only and doesn’t affect functionality.

getBundleById
Given a bundleId, locate and return the corresponding metadata from your storage solution. This should be fetched from your local snapshot of the data (which should be up-to-date if you’ve called getBundles recently).

getBundles
This method should return an array of bundle metadata representing all deployed bundles. If refresh is true, you should fetch the latest data from your remote storage and update your local snapshot accordingly.

updateBundle
Find the bundle in your local snapshot that matches the given bundleId and update its fields using the data in argument passed called newBundle. This allows you to partially modify bundle metadata without rewriting the entire object.

appendBundle
As the name suggests, this simply adds new bundle metadata to the end of your local snapshot. In my implementation, I extended the base Bundle object to include a patch_number field for easier version tracking.

commitBundle
Once you’ve finished modifying the local snapshot (via updateBundle or appendBundle), this method writes the updated snapshot back to your storage solution. If your getBundles method handles the refresh flag correctly, your local state and remote data should always stay in sync.


Deployments

Once your plugins are defined in your root config file, you can begin using the CLI commands provided by Hot Updater to execute deployments.

To start, you might find the interactive deployment mode helpful—it walks you through selecting a target release and platform:


As your workflow matures, especially with CI/CD integration, you can use the more direct command to initiate a patch deployment:

When provided with valid arguments, this command will:

  1. Bundle your current React Native project.
  2. Use your Storage Plugin to upload the bundle from the local machine to your configured storage solution.
  3. Use your Database Plugin to store the metadata associated with the bundle in your data storage provider.


Management

To edit metadata, remove deployments, or roll back patches, you can launch the Hot Updater Console from the CLI. This starts a local web server on your machine, which you can open in your browser at http://localhost:3000 (or another port, configurable in your setup).
You can read more about the console here.

To start the updater run the following CLI command in your projects root:

Open your web browser to localhost using the port you specified in your config. You’ll be presented with an interface that shows all deployed bundles, their status, and options to roll back a deployment or toggle the force update flag.


Native Setup

To enable Hot Updater in your project, you’ll need to modify the native code responsible for loading the JavaScript bundle. Replace the default bundle loading logic with Hot Updater’s bundle-swapping boilerplate:

iOS:
Update your AppDelegate.mm file with the bundle-swapping code found here:
Hot Updater iOS Integration Guide

Android:
Update your MainApplication.java file using the boilerplate provided here:
Hot Updater Android Integration Guide


React Native Setup

Now that we’ve configured patch deployment to our custom storage solution and integrated the bundle-swapping code into our native Android and iOS projects, the final step is to wrap the entry point of your React Native app with the HotUpdater.wrap higher-order component (HoC) provided by Hot Updater.

The HoC should be applied as high up as possilbe in your React Native entrypoint structure to allow you reach as much code.

 export default HotUpdater.wrap({ 
source: string | (() => Promise<Bundle[]>);
requestHeaders?: Record<string, any>;
reloadOnForceUpdate?: boolean;
onUpdateProcessCompleted?: ({ status, shouldForceUpdate, id, message }) => void;
onProgress?: ({ progress }) => void;
fallbackComponent?: ({ progress, status, message }) => ReactNode;
})(App);


HotUpdater.wrap Implementation Breakdown

source
This is one of the most important functions you’ll need to implement. It can either be a simple string pointing to a URL or a custom function that returns bundles relevant to the device’s platform and the app’s version number.

In my case, I established a custom key path structure on my S3 bucket, so I wrote a custom source function. It dynamically constructs a file path based on the app’s current state—such as release channel, version, and platform. For example, it checks within a path like:

It then scans for folders under that path that begin with version-prefixed names like “5_32_1“, “5_32_2“, and so on. These are used to build an array of bundles for that specific app version, platform, and environment.

requestHeaders
If you provide a URL as the source, you can also specify custom headers to be included with the REST requests made by Hot Updater..

reloadOnForceUpdate
If set to true, the user will be immediately interrupted and forced to reload the app if the latest patch bundle is marked as a force update.

onUpdateProcessCompleted
A callback that provides insights into the result of the update process. You can use it to track update success/failure, trigger manual reloads, or log analytics.

onProgress
A callback for monitoring the progress of the update download. Useful for displaying a progress bar, spinner, or any visual feedback to the user during the patching process.

fallbackComponent
A React component displayed while updates are being applied. Ideal for showing a branded loading screen, maintenance message, or activity indicator during the patch transition.


Find out more and stay up to date with their guide:
https://gronxb.github.io/hot-updater/guide/hot-updater/wrap.html


Finally!

Once you’ve configured the CLI with your custom plugins, updated your native code to support bundle swapping, and wrapped your React Native entry point with the HotUpdater.wrap HoC, you’re ready to use the patching system!

A few important tips before you go live:

Use the tools as intended. Until you’re completely comfortable with how everything works, avoid making manual changes to your storage system. Instead, manage deployments using the CLI and the built-in console. Some storage structures are more fragile than others—this is your warning.

Secure your storage solution. The bundle is an obfuscated version of your entire React Native codebase. Always abstract secrets and API keys from the app and never hardcode anything sensitive.

What you see is what you get. Before deploying a patch, make sure your local codebase is fully prepared. In my case, we stub in API keys and other environment-specific values during our CI process—so running a deployment locally could lead to issues if every step isn’t handled correctly. Always double-check that your local build mirrors your production environment to avoid accidental misconfigurations.

But otherwise… happy patching! 🎉

Leave a Reply