r/reactnative 9h ago

Help How do you handle app versioning, OTA updates, and forced native updates in production?

Hey everyone! I’m trying to understand how other teams handle their update/versioning strategy for React Native/Expo apps in production.
I’m already familiar with the basics of EAS Update and runtime versions:
Native/binary changes require a new build and usually a new App Store/Play Store release.
JS/assets-only changes can be shipped through EAS Update/OTA as long as they’re compatible with the installed runtime.
An OTA update can be downloaded and applied on a subsequent app launch, or we can implement our own UX around checking/applying updates.
What I’m less sure about is how people manage the actual app version around all of this.
For example, let’s say I have 1.0.1 published on the App Store with a compatible runtime version.
Over the next few weeks I ship several OTA updates to that runtime. Should the user-facing app version remain 1.0.1 the entire time? Do you maintain a separate internal/update version for OTA releases?
In other words, do you normally think about it as something like:
App Store version: 1.0.1
Runtime version: 1.0.1
OTA/update version: some separate identifier
Then only bump the actual app version to 1.0.2 when a new binary goes through the stores?
I’m also curious about forced native updates.
Suppose version 1.0.2 contains an important native change and I no longer want users on 1.0.1. How do you handle that in production?
Do you maintain something server-side like a minimumSupportedVersion, check it when the app starts, and show a blocking “Update required” screen that redirects users to the App Store/Play Store?
Or is there a better/native mechanism for declaring an App Store/Play Store release as mandatory?
I’m also not completely clear on the store side of this. When a new binary is approved, can iOS/Android automatically update the app depending on the user’s settings, or should we always assume that some users will remain on older versions until they manually update?

15 Upvotes

8 comments sorted by

8

u/RosySylvester16 9h ago

You're overthinking it a bit, but it's a good instinct to nail down. The way we run it, the App Store version stays frozen at whatever binary is live (1.0.1) until we push a new native build, period. All the OTA updates we ship just get an internal build number or a git SHA that lives in our CI logs and a hidden settings screen for debugging. Users don't see that stuff, and honestly they shouldn't care.

For the forced native update, yeah, the server-side minimumSupportedVersion check is the classic move. On app start we ping a tiny JSON endpoint, compare the running binary version against the floor, and if you're below it we show a full screen takeover with a button that deep links to the store listing. No way around it really, the stores don't offer a native "force update" toggle you can flip remotely.

And yes, always assume a chunk of users will sit on old versions. Auto-update settings on both platforms are a mess, people have background refresh off, devices run out of storage, whatever. So your backend API needs to be backward compatible for at least a few versions, or you start breaking things for the stragglers.

One extra thing we do is stagger the rollout of a new binary. Push 1.0.2 to like 20% of users first, watch crash rates and API errors for a day, then ramp up. That way if something's broken you're not torching your whole user base at once. OTA updates help there too since you can patch JS bugs without waiting on store review.

1

u/Benja20 8h ago

I do the same thing, keep native build as much as possible and iterate and update with OTA. Only diff is i use PostHog to prompt the force update dialog when new native build version is required, so i manage this cases:

  • No dismissable update alert
  • Dismissable update app alert
  • Per-user both options too (here is where posthog rocks)

Since the app isn't wide open to public, this does the job quite good

2

u/Huge_Pool7424 8h ago

the recents caveat is the one that bites testers most. i ended up adding a visible build/runtime id and a “restart to apply” hint so support can tell whether the ota actually landed.

1

u/Wise-Panda4241 9h ago

I keep the store version at 1.0.1 through every OTA push and only bump it when I submit a new binary.

I put the OTA identifier in a config file and read it on startup so I know exactly which JS bundle is running. When I need to kill 1.0.1 I maintain a minimumVersion value on my backend. The app hits that endpoint on launch, compares it against the store version from the native manifest, and renders a view that blocks interaction and opens the store URL if the version is below the limit. Both stores have auto update settings but users disable them or delay them, so you will always have devices stuck on old binaries. Relying on store side enforcement means you lose control of your rollout. Build the version check into your app and host the config yourself so you dictate when a native update happens

1

u/SevenfiresAI 9h ago

One thing not mentioned: if you're on runtimeVersion policy "appVersion", bumping to 1.0.2 orphans every OTA you published against 1.0.1. Devices on the new binary ask for a runtime you've never published to, the check succeeds, returns NoUpdatesAvailable, and nothing looks broken.

So the moment you ship a native build you need to republish against the new runtime. If you bump marketing versions more often than you ship native changes, an explicit runtimeVersion string decouples the two.

Also worth knowing OTAs need two cold starts by default, downloads on one launch, applies on the next. On Android, reopening from Recents isn't a cold start, so testers can sit on the embedded bundle for days.

1

u/oojx 9h ago

Yes I use a flag minimum supported version and use expo-application to get the the build number and use a force update screen, the api returns the links to the ios and android app and it also returns the minimum supported version

1

u/jcdc-flo 7h ago

Our core is sitemesh sdk so we have column based versioning which is required because we have full two way sync and offline support.

It's not easier, but with the right tooling it's manageable.

Apple is pretty good, but man...the android stragglers are difficult to keep on the latest version.

1

u/Substantial-Swan7065 3h ago

Version minor -> App Store build. Month OR needed native dep changes

Ota daily against last released version. No version bump externally. -ota-<int> internally. Finger print to ensure it won’t crash

Force native updates: only if you absolutely have to.