Keeping your iOS build numbers tidy across Expo, EAS, and App Store Connect can get confusing fast. If you’ve ever wondered why TestFlight shows 23 while your new build shows 26 (or why Apple rejects a build with a “duplicate build number” error), this guide is for you.
This article gives you a clean mental model and a copy‑pasteable workflow that works every time.
The Mental Model (Version vs Build Number)
-
Marketing Version (
expo.version
): The human‑readable app version users see (e.g.,1.0.3
). You change this when you ship a meaningful update to the store. -
Build Number (
ios.buildNumber
): The internal counter Apple uses to differentiate binaries for the same marketing version. It must strictly increase for a given version (e.g.,1.0.3 (25)
→1.0.3 (26)
).
When you bump the marketing version (e.g.,
1.0.3 → 1.0.4
), you can reset the build number back to"1"
(or whatever convention you use).
Step 1 — Pick a Single Source of Truth
Use app.config.ts
as your source of truth for versioning.
// app.config.ts
export default {
expo: {
name: "my-app",
slug: "my-app",
version: "1.0.3", // Marketing version
ios: {
buildNumber: "26", // Build number (string)
},
android: {
versionCode: 26, // Android integer (optional but recommended)
},
runtimeVersion: { policy: "sdkVersion" },
},
};
Why this works: EAS reads these values at build time and uploads a binary that App Store Connect accepts as 1.0.3 (26)
— exactly what you intend.
Step 2 — Find the Next Build Number You Should Use
You want to avoid collisions and rejections. Check Apple’s highest build number for the current marketing version, then use (highest + 1).
- In App Store Connect → TestFlight → filter by Version
1.0.3
→ note the highest build (e.g.,25
). - In EAS (sanity check your recent builds):
eas build:list --platform ios --limit 20
Rule: Apple’s highest build for version
1.0.3
wins. Your nextios.buildNumber
should be that + 1 (e.g.,26
).
Step 3 — Commit the Bump Before You Build
git add app.config.ts
git commit -m "chore(ios): bump buildNumber to 26 for 1.0.3"
Step 4 — Build with EAS (Preview or Production)
Use your usual profile (e.g., preview
or production
).
eas build --platform ios --profile preview
After it finishes:
- EAS uploads
1.0.3 (26)
to App Store Connect. - Confirm in TestFlight under Version
1.0.3
that your build appears correctly.
Step 5 — Keep OTA Channels Straight (for EAS Update)
Your binary targets a specific channel (e.g., preview
). All over‑the‑air updates must go to that same channel:
eas update --channel preview --message "Fix: Supabase config + trophies UI"
Channels do not impact build numbers, but they control where your OTA updates land.
When to Bump Version vs Build Number
-
JS‑only fixes/features: Don’t bump either. Just ship with
eas update
(OTA). -
New binary, same version: Bump
ios.buildNumber
only (e.g.,25 → 26
). -
Public release or native changes: Bump
version
(e.g.,1.0.3 → 1.0.4
) and resetios.buildNumber
to"1"
(or your chosen baseline).
Optional: Make Bumping Easy with Scripts
A) Manual bump via ENV (explicit and predictable)
// package.json
{
"scripts": {
"set:ios:build": "node scripts/set-ios-build.mjs"
}
}
// scripts/set-ios-build.mjs
import fs from 'node:fs';
const target = process.env.IOS_BUILD_NUMBER; // Usage: IOS_BUILD_NUMBER=26 npm run set:ios:build
if (!target) {
console.error('Provide IOS_BUILD_NUMBER env var'); process.exit(1);
}
let s = fs.readFileSync('app.config.ts', 'utf8');
s = s.replace(/buildNumber:\s*\"(\d+)\"/, `buildNumber: "${target}"`);
fs.writeFileSync('app.config.ts', s);
console.log(`iOS buildNumber set to ${target}`);
Usage:
IOS_BUILD_NUMBER=26 npm run set:ios:build
git commit -am "bump: iOS buildNumber 26"
eas build --platform ios --profile preview
B) One‑liner shortcut
{
"scripts": {
"ios:bump:26": "IOS_BUILD_NUMBER=26 npm run set:ios:build && eas build --platform ios --profile preview"
}
}
Android Note (Keep It in Sync)
Android uses versionCode
(integer) that must always increase, regardless of marketing version.
android: {
versionCode: 26
}
You can keep Android and iOS numbers aligned for sanity, or manage them separately — both are valid.
TL;DR Checklist
-
Check Apple’s highest build for current version (TestFlight → Version
1.0.3
). - Set
ios.buildNumber
inapp.config.ts
to (highest + 1). - Commit the change.
-
eas build --platform ios --profile preview
(or your prod profile). - Use
eas update --channel <channel>
for OTA JS/asset patches between binaries.
This workflow keeps App Store Connect, EAS, and your repo perfectly in sync — and spares you the late‑night “why did Apple reject this?” scramble. 🚀
Top comments (0)