If you’re a one‑person team with a few occasional testers, here’s a simple, durable process that balances speed (for your day‑to‑day iteration) with stability (when you need a new binary).
TL;DR
- Keep a TestFlight “store” preview build as your main testing track for friends & family.
-
Ship day‑to‑day fixes via
eas update
(OTA) to the same channel as that preview build. - Use EAS “internal distribution” builds only for quick smoke tests on your own device, not for broader testing.
Why this setup works
TestFlight “store” preview builds (recommended for friends & family)
- ✅ Easy install (no UDIDs), just a TestFlight link.
- ✅ Works for both Internal (App Store Connect users) and External testers (up to 10k).
- ✅ Apple’s analytics + crash data + build expiration handling.
- ⚠️ Requires App Store Connect processing; External builds require Beta App Review.
Pair this with EAS Update and you’ll avoid cutting a new binary for every small fix:
# JS/asset-only changes to the same runtimeVersion
eas update --channel preview --message "Minor fix"
EAS “internal distribution” builds (use sparingly)
- ✅ Super fast to install on your device (no Apple processing).
- ⚠️ Testers must share UDIDs; you must register devices (friction).
- ⚠️ Per‑year device limits; no TestFlight analytics/crash data.
Best for: you doing quick smoke tests before creating a TestFlight build.
A simple, durable workflow
1) Keep one TestFlight preview track (store builds)
# Build a store-distribution binary and submit to App Store Connect
eas build --platform ios --profile preview
eas submit --platform ios --latest
- After processing, add the build to your Internal group (instant).
- For External testers, add “What to Test” and submit for Beta App Review.
2) Iterate fast with OTA updates (no new binary)
# Publish JS/asset-only changes to the same channel targeted by that binary
eas update --channel preview --message "UI polish / copy tweaks"
3) Only rebuild when:
- You change native modules or config that requires a new binary.
- You want to bump the build number (same marketing version) or the marketing version for a milestone.
4) Use an internal build for your own device when you need a rapid smoke test:
eas build --platform ios --profile internal
# Install from the Expo build page (register your device once if needed)
Example eas.json
(copy/paste)
{
"cli": { "version": ">=16.20.1" },
"build": {
"preview": {
"channel": "preview",
"ios": {
"buildConfiguration": "Release",
"distribution": "store" <-- Apple App Store
}
},
"internal": {
"channel": "preview",
"ios": {
"buildConfiguration": "Release",
"distribution": "internal" <-- Expo ad-hoc
}
},
"production": {
"channel": "production",
"ios": {
"buildConfiguration": "Release",
"distribution": "store"
}
}
},
"submit": {
"preview": {
"ios": {},
...
},
"production": {
...
}
}
-
preview = your TestFlight track (store builds + OTA to
preview
) - internal = fast ad‑hoc builds for your device
-
production = App Store release track (+ OTA to
production
)
Versioning & build numbers (iOS)
- Small JS‑only fixes → don’t bump version; use OTA.
- New binary, same version → bump
ios.buildNumber
only (e.g.,25 → 26
). - Bigger release → bump
version
(e.g.,1.0.3 → 1.0.4
) and resetios.buildNumber
to"1"
(or your convention).
// app.config.ts
export default {
expo: {
version: "1.0.3",
ios: { buildNumber: "26" },
android: { versionCode: 26 },
runtimeVersion: { policy: "sdkVersion" }
}
}
export default {
expo: {
name: "PlanetFam Quiz",
slug: "my-app",
version: "1.0.3", <--- only become 1.0.4 if major
...
runtimeVersion: { policy: "sdkVersion" },
ios: {
supportsTablet: true,
bundleIdentifier: "com.cathyapp1234.my-app",
buildNumber: "28" <--- only increase for new build
},
android: {
package: "com.radiantleaf.planetfam",
versionCode: 28, <--- only increase for new build, same as above
....
},
Rule: Apple requires
buildNumber
to strictly increase for a givenversion
. When you changeversion
, you can resetbuildNumber
.
Channels & OTA (don’t mix them up)
- A binary targets a channel (e.g.,
preview
). - All OTA updates must be published to the same channel to reach that binary’s users:
eas update --channel preview --message "Fix trophy UI"
- Changing channel requires a new build (so the binary points at the new channel).
Quick decision guide
- “Just a text fix / minor JS polish?” → OTA (
eas update
). - “Upgraded a native package / changed config?” → Build + Submit.
- “Need to sanity check on my phone in 5 minutes?” → Internal build for you only.
- “Want friends & family to test?” → TestFlight (store) preview build + OTA updates to
preview
.
This approach keeps your testing fast and your releases tidy, without burning time on unnecessary rebuilds. 🚀
Top comments (0)