Versioning a React Native App: versionCode, buildNumber, runtimeVersion
React Native app versioning explained: version vs build number on iOS and Android, runtimeVersion for OTA updates, auto-increment and update checks.
Full-Stack, Mobile & AI Engineer
A React Native app has more version numbers than most people expect: one users see, one each store requires to go up with every upload, and, if you use over-the-air updates, one that decides which updates a build can run. Mix them up and you get rejected uploads, confused testers, or an update that crashes the builds it reaches.
This guide untangles React Native app versioning: what each number means on iOS and Android, how Expo maps them, how to automate them so they never collide, and how to tell users when they must update.
Key takeaways
- The app version (for example 1.4.0) is what users see. It's
CFBundleShortVersionStringon iOS andversionNameon Android. - The build number must increase with every upload:
CFBundleVersionon iOS and the integerversionCodeon Android. - In Expo, set
version,ios.buildNumberandandroid.versionCode, or let EAS manage build numbers with remote versioning andautoIncrement. runtimeVersionis separate: it marks which native code an over-the-air update needs, so updates only reach compatible builds.- A minimum-version check in your API lets you force updates when an old version must stop working.
The numbers, side by side
| Concept | iOS | Android | Expo app config | Who sees it |
|---|---|---|---|---|
| App version | CFBundleShortVersionString | versionName | version | Users, in the store |
| Build number | CFBundleVersion | versionCode (integer) | ios.buildNumber, android.versionCode | The stores and your team |
| Runtime version | (Expo concept) | (Expo concept) | runtimeVersion | expo-updates |
On Android, versionCode must be a whole number that increases with every upload to Google Play, up to a maximum of 2,100,000,000. On iOS, each upload needs a build number that hasn't been used for that version, and increasing it every time is the simplest rule.
A versioning scheme that works
- Use semantic-style versions for users: major.minor.patch, such as 1.4.0, then 1.4.1 for a fix and 1.5.0 for new features.
- Never reuse build numbers. Treat them as a counter that only goes up.
- Bump the app version for meaningful releases, and let build numbers increase on every build, including test builds.
- Keep iOS and Android on the same app version for the same release, even if their build numbers differ.
Let EAS manage build numbers
Manual build numbers break as soon as two people build on the same day. With EAS, store them remotely and increment them automatically:
{
"cli": { "appVersionSource": "remote" },
"build": {
"production": { "autoIncrement": true }
}
}That's eas.json. With "appVersionSource": "remote", EAS keeps the current build number for each platform on its servers, and autoIncrement bumps it on every production build. You still change version in your app config when you release a new user-facing version. If you're migrating an existing app, set the remote values to match what's already in the stores first, so the next build number is higher.
runtimeVersion: the number for over-the-air updates
If you use EAS Update, each build also has a runtime version. An over-the-air update is only delivered to builds with the same runtime version, which prevents JavaScript that expects new native code from running on a build that doesn't have it.
{
"expo": {
"version": "1.4.0",
"runtimeVersion": { "policy": "fingerprint" }
}
}With the fingerprint policy, the runtime version changes automatically whenever anything affecting native code changes. With appVersion, it follows your app version, which is simpler but relies on you bumping the version whenever native code changes. Either way, the app version and the runtime version answer different questions: "what do users call this release?" versus "which updates can this build run?"
Showing the version inside the app
Put the version and build number on your settings or about screen, so support conversations can start with facts:
import * as Application from "expo-application";
export function VersionLabel() {
return (
<Text>
Version {Application.nativeApplicationVersion} ({Application.nativeBuildVersion})
</Text>
);
}If you use over-the-air updates, showing the update ID or creation date from expo-updates as well makes it obvious which JavaScript a user is actually running.
Forcing users to update
Sometimes an old version must stop working: an API is being retired, a security fix is mandatory, or a payment flow changed. The reliable pattern is server-driven:
- Your API exposes a minimum supported version per platform, for example
{ "ios": "1.3.0", "android": "1.3.0" }. - The app compares it with its own version on launch.
- Below the minimum, show a blocking screen with a button to the store listing. Between "minimum" and "latest", show a gentle, dismissible prompt.
Keep the minimum as low as you can. Every forced update interrupts users, and some will be on devices that can't install the newest version.
From the field: Release ownership is part of what I do on the apps I ship: EAS builds and submissions, OTA hotfixes with EAS Update, phased rollouts and store review. Remote versioning with autoIncrement is worth setting up first, because a rejected upload over a duplicate build number is the most avoidable delay in the whole release process.Common versioning mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Reusing a build number | Upload rejected by App Store Connect or Google Play | Remote versioning with autoIncrement |
Forgetting to bump versionCode in a bare project | "Version code has already been used" | Increment in build.gradle or automate it |
| Native change without a new runtime version | OTA update crashes older builds | Use the fingerprint runtime policy |
| Different app versions for iOS and Android for the same release | Confused support and analytics | Release both with the same version |
| No version shown in the app | Slow bug reports | Show version, build and update ID in settings |
FAQ
What's the difference between versionName and versionCode?
versionName is the human-readable version users see, such as 1.4.0. versionCode is an integer that Google Play uses to tell builds apart, and it must increase with every upload. iOS has the same split with CFBundleShortVersionString and CFBundleVersion.
How do I increment the build number automatically in Expo?
Set "appVersionSource": "remote" under cli in eas.json, and "autoIncrement": true on your production build profile. EAS then stores the build number on its servers and increments it on every production build for both iOS and Android.
What is runtimeVersion in Expo?
It identifies the native code a build contains, for over-the-air updates. EAS Update only delivers an update to builds with the same runtime version, so JavaScript that depends on new native code never reaches an older build. The fingerprint policy updates it automatically when native code changes.
How do I force users to update my React Native app?
Store a minimum supported version per platform on your server, compare it with the installed version when the app starts, and show a blocking screen linking to the store when the app is too old. Use a softer, dismissible prompt for versions that are outdated but still supported.
Next steps
If your release process involves hand-edited build numbers and the occasional rejected upload, I can set up versioning, runtime versions and update checks so they take care of themselves. Get in touch, or see my services.
Sources
- App version management, Expo
- Runtime versions, Expo
- Version your app, Android Developers
- CFBundleVersion, Apple
- expo-application, Expo
Related
EAS Build Explained: Profiles, Credentials, and What It Costs
EAS Build explained for 2026: build profiles in eas.json, managed credentials, local builds, versioning, and what Expo's plans and per-build prices cost.
Code Signing Demystified: Certificates, Provisioning Profiles and Keystores
React Native code signing explained: iOS certificates and provisioning profiles, Android keystores, upload vs app signing keys, and how EAS manages it all.
Fastlane for React Native in 2026: When You Still Need It
Fastlane for React Native in 2026: what it automates, when EAS replaces it, when you still need it, and a working Fastfile for TestFlight and Google Play.
