Shipping & Release· 5 min read

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.

By

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 CFBundleShortVersionString on iOS and versionName on Android.
  • The build number must increase with every upload: CFBundleVersion on iOS and the integer versionCode on Android.
  • In Expo, set version, ios.buildNumber and android.versionCode, or let EAS manage build numbers with remote versioning and autoIncrement.
  • runtimeVersion is 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

ConceptiOSAndroidExpo app configWho sees it
App versionCFBundleShortVersionStringversionNameversionUsers, in the store
Build numberCFBundleVersionversionCode (integer)ios.buildNumber, android.versionCodeThe stores and your team
Runtime version(Expo concept)(Expo concept)runtimeVersionexpo-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:

  1. Your API exposes a minimum supported version per platform, for example { "ios": "1.3.0", "android": "1.3.0" }.
  2. The app compares it with its own version on launch.
  3. 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

MistakeSymptomFix
Reusing a build numberUpload rejected by App Store Connect or Google PlayRemote 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 versionOTA update crashes older buildsUse the fingerprint runtime policy
Different app versions for iOS and Android for the same releaseConfused support and analyticsRelease both with the same version
No version shown in the appSlow bug reportsShow 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

Bhuwanesh Sisodia

Bhuwanesh Sisodia

Full-Stack, Mobile & AI Engineer

Bhuwanesh Sisodia is a Full-Stack, Mobile & AI Engineer based in Mumbai, with 7+ years of experience and 50+ apps shipped to the App Store and Google Play. He works independently with startups and product teams worldwide.

About meGitHubLinkedInX

Related