Skip to main content

App Store Distribution & Release Strategy

TL;DR

TestFlight (iOS): Beta testing for up to 10,000 testers. 90 days max. Catches crashes before App Store release.

Play Console (Android): Internal testing, closed/open beta tracks. Gradual rollout (1% → 5% → 100%). Test smaller % first.

Gradual Rollout: Release to 1% of users, monitor crashes/feedback, expand if healthy. Catch issues affecting millions before they happen.

Versioning: Use SemVer (major.minor.patch). Communicate breaking changes. Support min OS versions.

Policies: App Store review (1-3 days), strict policies (no web apps, subscription disclosure). Plan review time into timeline.

Learning Objectives

You will be able to:

  • Set up beta testing (TestFlight, Play Console).
  • Plan gradual rollouts and monitor health metrics.
  • Understand app store policies and approval process.
  • Implement over-the-air update strategies (CodePush, Firebase).
  • Manage versioning and backward compatibility.

Motivating Scenario

You release v2.0 with breaking changes. All users auto-update. Within 1 hour, 10% of users hit a crash. Users flood support, leave bad reviews. App rating plummets. Recovery takes weeks.

With proper release strategy:

  • Beta phase: 1000 TestFlight testers catch crash
  • Gradual rollout: Release to 1%, monitor. Crash detected after 100 users, not 1 million
  • Quick rollback: Within 2 hours, rollback to v1.9
  • Loss: 100 users disappointed vs. 1M

Beta Testing

iOS: TestFlight

Test before App Store release:

Distribution.swift
// 1. Build signed app for TestFlight
// In Xcode:
// - Product → Archive
// - Distribute App → TestFlight & App Store
// - Select "TestFlight Only"
// - Upload

// 2. Configure tester groups in App Store Connect
// - TestFlight tab
// - External Testers: add email addresses
// - Send invitations

// 3. Testers download app from TestFlight app
// - Fixed for 90 days
// - Can test multiple builds
// - Feedback via built-in form

// 4. Monitor crashes
// - Crashes tab shows stack traces
// - Session logs available
// - Real-world device diagnostics

Android: Play Console

Multi-track beta testing:

build.gradle
android {
// Build for different tracks
flavorDimensions "track"
productFlavors {
// Production: live on Play Store
production {
dimension "track"
applicationIdSuffix ""
}
// Beta: only for beta testers
beta {
dimension "track"
applicationIdSuffix ".beta"
versionNameSuffix "-beta"
}
// Internal: internal team only
internal {
dimension "track"
applicationIdSuffix ".internal"
versionNameSuffix "-internal"
}
}
}

// In Play Console:
// - Internal testing: instant, small team
// - Closed beta: selected testers (max 500 for closed, unlimited for open)
// - Staged rollout: 1% → 5% → 10% → 100%

Gradual Rollout Strategy

Release in stages, monitor metrics:

Stage 1: 1% (10,000 users if 1M base)

  • Monitor crash rate (target: <0.1%)
  • Monitor ANR rate (target: <0.5%)
  • Session length stability
  • Error logs

Stage 2: 5% (expand if healthy)

  • Larger sample size for confidence
  • Check for regional issues (network, locale)
  • User feedback

Stage 3: 25-50% (expand further)

  • High confidence in stability
  • Performance metrics across device types

Stage 4: 100% (full release)

  • All users receive update

Metrics to monitor:

ReleaseMonitoring.kt
// Monitor these metrics during rollout
data class HealthMetrics(
val crashRate: Double, // Crashes per session
val anrRate: Double, // Application Not Responding
val sessionLength: Double, // Average session duration
val errorRate: Double, // API errors, unhandled exceptions
val performanceImpact: Double, // Slow downs vs previous version
)

fun shouldExpandRollout(metrics: HealthMetrics): Boolean {
return metrics.crashRate < 0.001 // <0.1%
&& metrics.anrRate < 0.005 // <0.5%
&& metrics.sessionLength > 0.9 // >=90% of previous
&& metrics.errorRate < 0.02 // <2%
}

fun shouldRollback(metrics: HealthMetrics): Boolean {
return metrics.crashRate > 0.01 // >1%
|| metrics.sessionLength < 0.7 // <70% of previous
}

Versioning & Compatibility

Semantic Versioning

major.minor.patch (e.g., 2.5.3)

major: breaking changes (API changes, min OS bump)
minor: new features (backward compatible)
patch: bug fixes

Example timeline:
v2.0.0: Requires iOS 14+ (breaking, needs upgrade)
v2.1.0: New search feature (backward compatible)
v2.1.1: Fix crash bug (patch)

Communicate Changes

# Version 2.0.0 Release Notes

## New Features
- Dark mode support
- Offline-first editing

## Breaking Changes
- Requires iOS 14+ (dropped iOS 13 support)
- Old login method removed (migrate to OAuth)

## Migration Guide
[Link to migration docs]

## Bug Fixes
- Fixed crash on startup

Support Min OS Version

Know your user base:

VersionSupport.kt
// Check version at runtime
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// API level < 23 (Android 6)
// Disable features not available
disableNativeLib()
useBackupImpl()
} else {
useNativeLib()
}

// Track users by OS version
analytics.trackEvent("AppOpened", {
"osVersion" = Build.VERSION.SDK_INT,
})

// Decision: How many OS versions to support?
// Common: current + 2 previous (3 versions)
// Aggressive: current only
// Conservative: current + 3-4 previous

Over-the-Air Updates

Update without app store approval (JavaScript/assets only):

CodePush (React Native)

CodePushSetup.js

const App = () => {
return <MainApp />;
};

export default CodePush(
// deployment key
{ deploymentKey: "abcd1234" }
)(App);

// In AppCenter:
// - Upload new JavaScript bundle
// - Target staging (test) or production
// - Gradual rollout: 1% → 100%
// - Users download on next app launch or check

Firebase Remote Config

RemoteConfigUpdate.kt
val remoteConfig = Firebase.remoteConfig
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
if (task.isSuccessful) {
val forceUpdate = remoteConfig.getBoolean("force_update")
if (forceUpdate) {
// Prompt user to update from App Store
showUpdateDialog()
}

// A/B test features
val newFeatureEnabled = remoteConfig.getBoolean("new_feature")
if (newFeatureEnabled) {
enableNewFeature()
}
}
}

App Store Policies

Apple App Store

  • Review time: 24-48 hours (can be <24h)
  • Rejection common reasons: crashes, external links, payment outside IAP
  • Private APIs not allowed
  • Template apps rejected
  • Subscriptions must disclose (show pricing)

Google Play Store

  • Review time: 2-4 hours typical
  • Staged rollout (1% default)
  • Less strict than Apple
  • Policy violations: spam, malware, policy abuse

Handling Rejections

# Checklist to avoid rejections:
- [ ] App doesn't crash on startup
- [ ] Privacy policy linked
- [ ] Subscription pricing shown clearly
- [ ] No external payment methods
- [ ] All promised features work
- [ ] Age-appropriate content rating
- [ ] No test data left in release build
- [ ] Proper permissions usage (don't ask for more than needed)
- [ ] Tested on min and max OS versions
- [ ] Screenshots and description accurate

Design Review Checklist

  • Is beta testing set up (TestFlight/Play Console)?
  • Are gradual rollout metrics defined (crash rate, ANR)?
  • Is rollback procedure documented?
  • Does app support min OS version?
  • Are breaking changes documented?
  • Is migration guide provided if needed?
  • Are all promised features working?
  • Is privacy policy linked?
  • Are screenshot/store listing accurate?
  • Is team trained on app store policies?

When to Use / When Not to Use

Gradual Rollout When:

  • 10,000 active users

  • Feature impact unknown
  • Major changes
  • Depends on third-party service

Direct 100% Release If:

  • Bug fix with low risk
  • Small user base
  • Internal app only

Showcase: Release Timeline

Day 1: Submit to App Store
Day 2: App Store review completes
Kick off 1% rollout (monitor)
Day 3: 1% metrics healthy → expand to 5%
Day 4: Monitor 5%
Day 5: 5% metrics healthy → expand to 25%
Day 6: Monitor 25%
Day 7: 25% metrics healthy → release to 100%
Day 8: Full rollout complete, monitor
Day 10: Remove previous version from archive (if rollback not needed)
Typical release timeline

Self-Check

  1. Why use gradual rollout instead of releasing to 100% immediately?
  2. What metrics would signal you need to rollback?
  3. How would you handle a user base split between iOS 14 and iOS 13?

Next Steps

One Takeaway

info

Beta testing catches issues before millions of users. Gradual rollouts prevent disasters (1% catch issues vs 100%). Monitor crash rate, ANR, session length. Plan for 3-5 day release cycle (review + testing). Have rollback procedures ready.

References

  1. Apple TestFlight
  2. Google Play Console
  3. CodePush for React Native
  4. Firebase Remote Config
  5. Apple App Store Review Guidelines