This content originally appeared on DEV Community and was authored by Cathy Lai
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.buildNumberonly (e.g.,25 → 26). - Bigger release → bump
version(e.g.,1.0.3 → 1.0.4) and resetios.buildNumberto"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
buildNumberto 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. 
This content originally appeared on DEV Community and was authored by Cathy Lai