Boolean Build

Versioning an Expo app: version vs buildNumber vs versionCode

Three numbers that look interchangeable and aren’t. Get them wrong and you learn about it at the worst possible time — mid-upload.

1. version — the one users see

expo.version in app.json is the marketing version (“1.4.0”). It shows in the store listing and becomes iOS’s CFBundleShortVersionString and Android’s versionName. Bump it when you ship to users.

2. ios.buildNumber — the one the store tracks

expo.ios.buildNumber maps to CFBundleVersion. App Store Connect refuses a second upload with the same build number, so it must increase on every upload — even a fix for the same user-facing version. And it must be **period-separated integers** (12 or 1.4.0, never 1.4-beta).

3. android.versionCode — an integer that only goes up

expo.android.versionCode is a plain integer Google Play uses to order uploads. Same rule: strictly increasing, never reused.

The classic mistake

You bump version to 1.4.0, re-upload to fix a typo in the listing, and get:

ERROR: The bundle version must be higher than the previously uploaded version.

Because you bumped the marketing version and not the build number. They’re separate on purpose.

The fix: let CI own the build number

Don’t type these by hand — derive them so they can’t go backwards. In app.config.js:

export default {
  expo: {
    version: '1.4.0',
    ios: { buildNumber: String(process.env.BUILD_NUMBER ?? '1') },
    android: { versionCode: Number(process.env.BUILD_NUMBER ?? 1) },
  },
};

Then feed BUILD_NUMBER from your CI run number. Every upload gets a fresh, larger number automatically — no more “already used”.

Check it before you upload

Missing or malformed build numbers are a common App Store bounce. ipa-check reads a built .ipa and tells you if CFBundleVersion is missing or isn’t the right shape.

npx @njweber/ipa-check ./YourApp.ipa