DEV Community

Cathy Lai
Cathy Lai

Posted on

How to sync iOS build numbers (Expo + EAS + App Store Connect)

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" },
  },
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Rule: Apple’s highest build for version 1.0.3 wins. Your next ios.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"
Enter fullscreen mode Exit fullscreen mode

Step 4 — Build with EAS (Preview or Production)

Use your usual profile (e.g., preview or production).

eas build --platform ios --profile preview
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 reset ios.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"
  }
}
Enter fullscreen mode Exit fullscreen mode
// 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}`);
Enter fullscreen mode Exit fullscreen mode

Usage:

IOS_BUILD_NUMBER=26 npm run set:ios:build
git commit -am "bump: iOS buildNumber 26"
eas build --platform ios --profile preview
Enter fullscreen mode Exit fullscreen mode

B) One‑liner shortcut

{
  "scripts": {
    "ios:bump:26": "IOS_BUILD_NUMBER=26 npm run set:ios:build && eas build --platform ios --profile preview"
  }
}
Enter fullscreen mode Exit fullscreen mode

Android Note (Keep It in Sync)

Android uses versionCode (integer) that must always increase, regardless of marketing version.

android: {
  versionCode: 26
}
Enter fullscreen mode Exit fullscreen mode

You can keep Android and iOS numbers aligned for sanity, or manage them separately — both are valid.


TL;DR Checklist

  1. Check Apple’s highest build for current version (TestFlight → Version 1.0.3).
  2. Set ios.buildNumber in app.config.ts to (highest + 1).
  3. Commit the change.
  4. eas build --platform ios --profile preview (or your prod profile).
  5. 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)