Articles · How-To GuidesUpdated October 2026

Your app has two version numbers, and the one that blocks uploads is the one users never see.

Every app carries a user-facing version and an internal build number. The version is the release number on your store listing, and you choose it. The build number identifies one attempt at shipping that release, and it has to be different on every upload, which in practice means higher. Get them the wrong way round and a perfectly good binary is refused before review even starts. On iOS the two keys are CFBundleShortVersionString and CFBundleVersion. On Android they are versionName and versionCode. Every field name and rule below comes from Apple's, Google's and Expo's own documentation, read on 10 October 2026. Most people meet this the first time they are uploading a build.

What follows is which field is which, which one the stores actually check, what the rejection looks like when you forget, and the exact lines in the project where both numbers live. Then what changes when a build service keeps the counter for you, which is the part that stops this ever being your problem again.

Start here

The short version

Raise the build number on every upload. Change the version when users should notice.

The version string is for humans. It can sit at 1.4.0 for a month while you upload ten builds of it to testers. The build number is for the store, and it is the one that has to move. Google states the rule plainly: you cannot upload an APK with a version code you have already used, and each successive release should use a greater value. Apple does not word it as a comparison, but the build string has to identify one build, so a repeat is refused.

The numbers are not interchangeable in shape either. An Android version code is a single positive integer, no dots allowed. An iOS build string is one to three period separated integers, digits and periods only. A value like 1.0-beta2 is legal in neither place. That is most of what goes wrong here, and all of it is avoidable before you ever press upload.

What is the difference between the version and the build number?

One is a name for a release. The other is a serial number for a binary. Apple documents CFBundleShortVersionString as the release or version number of the bundle, with a required format of three period separated integers such as 10.14.1, and nothing in it but digits and periods. That is the number on your product page. CFBundleVersion is documented as the version of the build that identifies an iteration of the bundle. Apple calls it machine-readable, which is a polite way of saying no one else is meant to read it.

Android splits the same way with blunter names. versionName is a string used as the version number shown to users, and Android's own documentation says it is the only value displayed to users. It can be a raw string or a reference to a string resource, and it does not have to look like a number at all. versionCode is a positive integer used as an internal version number, higher meaning more recent, and Android adds that apps and publishing services should not show it to users.

The Android version code does one job the iOS build string does not. The system uses it to protect against downgrades, by preventing a person from installing an APK whose version code is lower than the one already on their device. So the number is not only paperwork for the store. It decides whether an install is allowed to proceed at all. Android's page also notes that many developers treat semantic versioning as a reasonable basis for the user-facing string, which is a suggestion and not a requirement.

Two habits follow from this. Pick the version when you decide what the release is, and write it down once. Treat the build number as a counter you never think about, because the only correct value is a number you have not used yet. Shipping an unchanged version with a changed build number is normal and expected. It is what every beta cycle looks like.

Apple, CFBundleVersion, Information Property List reference, read 10 October 2026

Before you upload

Which number do I change?

Pick what you are about to send. The field names are the ones in an Expo app config.

iOS: Raise ios.buildNumber. Leave version alone.

Android: Nothing, unless you are uploading the same code to Play.

Testers see the same version string with a new build beside it. That is the normal shape of a beta cycle.

Which number has to go up, and what happens if it does not?

Android answers this in one sentence. You cannot upload an APK to the Play Store with a version code you have already used for a previous version. The guidance around it is to set any positive integer you like, but make sure each successive release uses a greater value, and the documented ceiling for Google Play is 2100000000. Typically you release the first version with a version code of 1 and increase it by one on every release after that, whether the release is large or small.

Apple is less direct, and this is worth saying plainly because the premise of the question does not quite match the documentation. Apple's reference for CFBundleVersion does not say the value must be greater than the last one. It says the key is required by the App Store, that it is used throughout the system to identify the version of the build, and, for macOS apps specifically, to increment the build version before you distribute a build. App Store Connect Help adds that the build string is used to uniquely identify the build throughout the system. Uniqueness is the enforced rule. Increasing the number is just the only scheme that cannot accidentally repeat.

What a repeat looks like is an upload that fails, not a review that fails. The binary is refused on the way in, which is why people first read it as a build problem. Expo's build documentation names the cause: a duplicate version number is one common reason for app store rejections, and it happens when the developer-facing build version was not incremented before the new build was made. There is nothing to appeal and nothing to fix in the code. Raise the number, rebuild, upload.

The user-facing version has a softer rule with one hard edge. You choose it, and App Store Connect asks you to enter an incremental App Store version number when you create a new version to release. Keep the same version string across a run of test builds, then move it when the release is the thing that changed. If you are rolling a new version out gradually, a phased release works on that version string, not on the build number.

Android Developers, version your app, read 10 October 2026

Where do these numbers live in a React Native project?

In an Expo project, three properties in the app config file, and Expo writes them into the native files during the build. version is the user-facing one: it becomes versionName in android/app/build.gradle and CFBundleShortVersionString in Info.plist. ios.buildNumber becomes CFBundleVersion in Info.plist. android.versionCode becomes versionCode in android/app/build.gradle. That is the whole map, and it is worth memorising, because every error message you will read names the native key rather than the one you edited.

In a bare Android project the same two values sit in the Gradle build file, inside the android block and then the defaultConfig block, as versionCode and versionName. You can override them per build type or per product flavour, and Gradle merges whatever wins into the app manifest during the build. That merge is why editing the manifest by hand rarely does what people expect. The build file is the source of truth, not the XML.

If you want either number inside the app, for a settings screen or a bug report, read it at runtime instead of hardcoding it. The expo-application library exposes Application.nativeApplicationVersion for the user-facing version and Application.nativeBuildVersion for the build version. That matters more than it sounds: once a build service increments the counter for you, the value in your config file and the value in the shipped binary are no longer the same thing, and only the runtime value is true.

Expo also warns about one case that bites existing React Native projects. If native code already carries version values, the native values take precedence, while expo-constants and expo-updates read from the app config. Keeping the two in sync is especially important with a nativeVersion runtime policy, because mismatched versions can deliver updates to the wrong version of an app. If your builds run through EAS, the building with EAS walkthrough covers the profiles these settings sit in.

Android Developers, version your app, set app version information, read 10 October 2026

Can a build service keep the counter for me?

Yes, and it is the fix for the only failure mode in this whole subject. EAS Build can hold your build numbers on its servers rather than in your repository. You set cli.appVersionSource to remote in eas.json, then set autoIncrement to true on the build profile you use for releases. Expo calls the remote source the recommended behaviour from EAS CLI version 12.0.0. The remote counter starts from whatever is in your project, so a version code of 1 in the app config becomes 2 on the first remote build, and a project with nothing set starts at 1.

One consequence surprises people. Once the remote source is on, the build version values in your app config are ignored and are not updated when the number is incremented remotely. Expo says you can safely remove them. If you have already shipped and want the counter to continue from the real store values rather than from 1, run eas build:version:set, pick the platform, and enter the last number you used in the stores. To pull the remote values back down for a local Xcode or Android Studio build, eas build:version:sync does that.

The local alternative is honest about its cost. With a local version source, EAS reads your project and does not write to it, and autoIncrement means you have to commit the changed file after every build for the increase to persist, which Expo notes is hard to coordinate on CI. Two more limits are worth reading before you turn any of it on: autoIncrement does not apply to the user-facing version property, and remote versioning is not supported with an EAS Update runtime policy of nativeVersion, where the appVersion policy is suggested instead.

Whichever you pick, write down which one you picked. The failures in this area are almost never a misunderstanding of the rules. They are two people, or a person and a pipeline, both believing they own the number. Make the counter one system's job, and bumping it part of ordinary app maintenance rather than something you remember at upload time.

Apple, CFBundleVersion, format and App Store requirement, read 10 October 2026

The same two numbers, named differently on each platform

The questioniOSAndroidSame on both
The number users readCFBundleShortVersionString, three period separated integersversionName, any string you likeYes
The internal counterCFBundleVersion, the build stringversionCode, a positive integerNo
Field in the Expo app configios.buildNumberandroid.versionCodeNo
Where it ends up nativelyInfo.plistandroid/app/build.gradleNo
Highest value the store documentsApple publishes no ceiling2100000000No
Reusing a number you already uploadedRejected, the build string has to identify one buildRejected, a version code cannot be reusedYes
Blocks a downgrade on the device itselfNoYesNo

Where this lands if a builder generated your app

Newly is an AI app builder. You describe an app in plain English and it writes a real React Native and Expo project that you own, which means the version fields are ordinary configuration in your own repository rather than a setting hidden in someone's dashboard. It uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK, and it keeps two-way GitHub sync, so you can change version or ios.buildNumber in the editor or in the repository and the other side sees it. It is $25 a month, with no free tier.

The practical effect is that every rule above still applies to you, unchanged. Each TestFlight upload needs a build number nobody has used. Each Google Play internal testing upload needs a version code nobody has used, because the no-reuse rule covers every track, not just production. The one step Newly deliberately does not take is the final submission on iOS, because App Store Review Guideline 4.2.6 requires apps built from an app generation service to be submitted by the provider of the content, which is you.

So the honest division of labour: a builder can write the fields and run the build, and you decide what a release is. Nothing can decide that for you, because the version string is a claim about what changed and only you know whether anything did. Set the version when you name the release. Let something else count the builds.

Questions people ask about app version and build numbers

The version is the release number people read on your store listing. The build number is an internal counter for one attempt at shipping that release. On iOS they are two Info.plist keys: CFBundleShortVersionString, which Apple calls the release or version number of the bundle, and CFBundleVersion, which Apple calls the version of the build that identifies an iteration of the bundle. On Android they are versionName, which Android's documentation calls the only value displayed to users, and versionCode, a positive integer used as an internal version number. Version 2.1 can have twenty build numbers behind it. Users never see any of them.

Build it, then let the counter look after itself

Decide the version once per release, automate the build number, and this stops being a thing you think about on upload day. The first upload is the one worth doing early, because it is where every numbering mistake shows up.

Start building