DEV Community

Cover image for Shipping React Native Updates Without the App Store: A Practical Guide to OTA and React Native Stallion
Amanda Gama
Amanda Gama

Posted on

Shipping React Native Updates Without the App Store: A Practical Guide to OTA and React Native Stallion

You know the feeling. There's a typo in production copy. A misconfigured feature flag. A small logic bug that snuck past QA somehow. None of it touches native code. It's pure JavaScript, the kind of fix you could push in five minutes if the universe were fair. But the universe isn't fair, and so you wait. You wait for Apple. You wait for the Play Store staged rollout. Hours go by. Sometimes days. All for a one-line change.

This is the gap OTA updates were built to fill. In this article I want to walk through what OTA actually means in React Native land, why everything changed in 2024, and how to get up and running with React Native Stallion, which is probably the most interesting CodePush replacement to come out of that whole mess.

What OTA Updates Actually Are (and Aren't)

A React Native app is really two things in a trench coat. There's the native shell, which is your APK or IPA with all its Java, Kotlin, Objective-C and Swift. And then there's the JavaScript bundle, which is just a file that the shell loads when the app starts. The app stores care about the shell. The bundle is, from their perspective, content.

OTA exploits that gap. Instead of resubmitting the whole app, you swap the JavaScript bundle on the device. Next launch (or whenever you decide), your users are running new code.

What you can ship over the air:

  • UI changes, copy fixes, styling tweaks
  • New features written entirely in JavaScript
  • Business logic updates, API integration changes
  • Bug fixes that don't touch native modules

What you can't:

  • Updates to native modules. Added a new library with native code? Time to rebuild.
  • Permission changes. Camera, location, notifications, all of that.
  • App icons, splash screens, native configuration
  • Anything below the JavaScript layer

Both Apple and Google allow JavaScript-only OTA. They've been pretty consistent about it. What they don't allow is using OTA to fundamentally change what your app does after the fact. Hot-fixing bugs is fine. Quietly turning your meditation app into a casino is not.

Why the Whole Landscape Shifted

For years the answer to "how do we do OTA on React Native" was basically: CodePush, paired with App Center, both from Microsoft. It was free, it was well-documented, it worked. Most teams just used it and got on with their lives.

Then in 2024 Microsoft announced they were retiring App Center, with the full shutdown landing in 2025. CodePush as a hosted service went with it.

That left a lot of teams suddenly squinting at their production apps wondering what now. The replacements that emerged:

Expo EAS Update is polished and well-maintained, but tightly coupled to the Expo ecosystem. If you're already there, great. If you're on bare React Native, there's friction.

Self-hosted CodePush is technically possible since the server code is open source. The catch is you're now running infrastructure, scaling it, securing it, and on call for it.

React Native Stallion is newer, fully managed, and was built specifically to fill the CodePush void. It also makes some interesting technical bets (differential patches, bundle signing) that the older tools don't.

The rest of this article focuses on Stallion, because it's what I keep seeing teams reach for when they're migrating off CodePush and don't want to fully commit to the Expo workflow.

What Stallion Actually Gives You

A few things stand out.

Patch updates. Traditional OTA ships the entire JavaScript bundle every time. Your bundle is 20 MB, you fix a one-line bug, every user on the planet downloads 20 MB. Stallion computes a file-level diff and ships only the changed bytes. Their docs claim up to 98% size reduction depending on the change, and from what I've seen that's roughly right. On slow connections this is the difference between an update finishing in seconds and an update never finishing at all.

Bucket-based testing and promotion. Bundles get uploaded to buckets and promoted to production from the dashboard. You can also push a build to internal testers without going through TestFlight or the Play Console, which is honestly the part QA teams get most excited about.

Automatic and manual rollback. Misbehaving release? You can pause it (stops new downloads) or roll it back (reverts existing devices on next launch). Crash-on-startup automatic rollback is also baked in, which I'll get to.

Bundle signing. Every bundle is cryptographically signed, with optional customer-managed keys. This matters more than people realize. An unsigned OTA pipeline is basically a supply chain attack waiting to happen.

Open source SDK and CLI. MIT licensed. The console is hosted.

Setting It Up

Stallion has three pieces. The CLI uploads bundles. The SDK on the device downloads and applies them. The console is where you manage releases. You'll need React Native 0.69 or higher.

1. Install the SDK and CLI

# in your React Native project
npm i react-native-stallion

# globally or in your CI tooling
npm i -g stallion-cli
Enter fullscreen mode Exit fullscreen mode

Then npx pod-install to link iOS.

2. Wire Up the Native Side

You're telling React Native to load its JavaScript bundle from Stallion's storage instead of the default location. On Android that means overriding getJSBundleFile. On iOS, bundleURL.

Android (React Native 0.76+, Kotlin):

import com.stallion.Stallion

override val reactNativeHost: ReactNativeHost =
  object : DefaultReactNativeHost(this) {
    override fun getJSBundleFile(): String? {
      return Stallion.getJSBundleFile(applicationContext)
    }
  }
Enter fullscreen mode Exit fullscreen mode

iOS (React Native 0.76+, Swift):

import react_native_stallion

override func bundleURL() -> URL? {
  #if DEBUG
    return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
  #else
    return StallionModule.getBundleURL()
  #endif
}
Enter fullscreen mode Exit fullscreen mode

The #if DEBUG matters. In development you want Metro serving fresh bundles, not Stallion. OTA only kicks in for release builds.

3. Add Your Project ID and App Token

Generate both from the Stallion console under Project Settings > Access Tokens, then drop them into your native config.

iOS, in Info.plist:

<key>StallionProjectId</key>
<string>your_project_id</string>
<key>StallionAppToken</key>
<string>spb_your_app_token_here</string>
Enter fullscreen mode Exit fullscreen mode

Android, in res/values/strings.xml:

<string name="StallionProjectId">your_project_id</string>
<string name="StallionAppToken">spb_your_app_token_here</string>
Enter fullscreen mode Exit fullscreen mode

4. Wrap Your Root Component

import { withStallion } from 'react-native-stallion';

const App = () => {
  // your app
};

export default withStallion(App);
Enter fullscreen mode Exit fullscreen mode

That's the whole setup. A new release build of your app will now check Stallion for updates on launch and when it returns to the foreground.

Publishing and Promoting an Update

The actual workflow is pretty boring once you've done it once, which is what you want.

  1. Make your code change.
  2. Build the JavaScript bundle and upload it to Stallion via the CLI.
  3. In the console, pick the bundle and click "Promote Bundle." You specify the target app version, write release notes, and optionally set a rollout percentage.
  4. Next time a user's app comes to the foreground, it checks for updates and pulls the new bundle in the background.

The rollout percentage is the part worth lingering on. By default new releases start at 0%, meaning only users explicitly logged into the SDK (typically your team) get the update. You then dial it up. 5%, 25%, 50%, 100%, watching adoption and crash metrics as you go. It's the same phased rollout pattern you'd use on the Play Store, except you're holding the dial directly instead of submitting paperwork to Google.

Custom Update UX

By default Stallion downloads silently in the background and the new bundle takes effect on the next cold start. For most apps that's the right behavior. Sometimes though you want to prompt the user, especially for important fixes. "Hey, there's an update ready, want to restart now?"

The SDK exposes a hook for exactly this:

import { useStallionUpdate, restart } from 'react-native-stallion';

const UpdateBanner = () => {
  const { isRestartRequired } = useStallionUpdate();

  if (!isRestartRequired) return null;

  return (
    <View style={styles.banner}>
      <Text>A new update is ready.</Text>
      <Button title="Restart" onPress={restart} />
    </View>
  );
};
Enter fullscreen mode Exit fullscreen mode

useStallionUpdate also gives you metadata about the new bundle, including release notes and version info, so you can surface something meaningful rather than a generic "update available" toast.

When Things Go Wrong

This is the part most teams underestimate when they're picking an OTA platform. You will, eventually, ship a broken bundle to production. The question is how fast you can recover.

Stallion gives you two levers from the dashboard.

Pause stops the release from downloading to any new device. Devices that already have it stay on it. Useful when you've spotted a problem but it's not severe enough to roll back.

Rollback pauses the release and tells devices that already pulled it to revert to the previous stable bundle on next launch. This is the nuclear option, and it's the one you want when something is really wrong.

There's also automatic rollback for the worst case: if a new bundle causes the app to crash before it can mark itself as healthy, the SDK reverts on next launch. This is the safety net that lets you sleep through a 3 AM bad deploy. (Not that you should, but you can.)

Some Honest Tradeoffs

OTA is a force multiplier, but it's not free. A few things worth chewing on before you commit.

You're adding a runtime dependency on a vendor. Stallion is well-maintained and the SDK is open source, but your update pipeline now depends on their availability. Worth understanding their SLA story before you wire critical workflows around it.

Native code changes still go through the app store. OTA doesn't reduce that cadence. It just lets you patch JavaScript faster between native releases.

Testing discipline matters more, not less. Faster shipping is also faster shipping of bugs. The teams that get the most out of OTA are the ones with solid CI, automated tests, and the discipline to actually use phased rollouts. Without that you've just built a faster way to break production.

App Store review policies still apply. Don't use OTA to dodge review. Apple has been clear that meaningful changes to app functionality need to go through the normal process.

Wrapping Up

If you're running React Native in production in 2026, OTA isn't really optional anymore. Your competitors have it. Your users expect bug fixes in days, not weeks. The "wait for the next release cycle" argument basically never wins against "patch it tonight."

CodePush is gone. The choices are Expo, build your own, or pick up something like Stallion. For teams on bare React Native who want CodePush-style ergonomics with some modern conveniences (patch updates, signed bundles, dashboard rollouts), Stallion is a reasonable default to evaluate.

Setup is an afternoon. Using it well takes longer, but that's true of any deployment pipeline. Start with internal beta. Ramp through phased rollouts. Watch your crash metrics. Keep the rollback button close.

Top comments (1)

Collapse
Β 
mamoor_ahmad profile image
Mamoor Ahmad β€’

Solid guide.
The "two things in a trench coat" analogy is spot on for explaining OTA to non-technical stakeholders.

The honest tradeoffs section is what sold me. Most OTA articles skip straight to "ship faster!" without mentioning that faster shipping means faster shipping of bugs too. The point about testing discipline mattering more with OTA, not less, is something every team learns the hard way once.

One thing worth adding:
bundle signing is non-negotiable if you're shipping OTA in production. An unsigned update pipeline is a supply chain attack vector that most teams don't think about until someone points it out.
Glad Stallion bakes that in rather than leaving it as an afterthought.
πŸ‘οΈπŸ™ŒπŸ‘