- Release Overview
- iOS App Store — Certificates and Provisioning
- Google Play Store — AAB and Signing
- CodePush and OTA Updates
- Expo Updates (EAS Update)
- Versioning Strategy
- Staged Rollout and Release Management
- Pre-Release Checklist
- Interview Questions & Answers
- Navigation
Publishing a React Native app involves native binary releases (App Store / Play Store) and optionally over-the-air (OTA) JavaScript updates for non-native changes.
┌──────────────────────────────────────────────────────────┐
│ Release Pipeline │
├──────────────┬─────────────────────┬─────────────────────┤
│ Build │ Store Submission │ OTA (optional) │
│ iOS .ipa │ App Store Connect│ CodePush / EAS │
│ Android AAB│ Google Play Console│ JS/asset updates │
└──────────────┴─────────────────────┴─────────────────────┘
| Release Type | Can Update | Review Required | Use Case |
|---|---|---|---|
| Store release | Native + JS | Yes (1-3 days) | New native modules, permissions |
| OTA update | JS/assets only | No | Bug fixes, UI changes, logic |
- Enroll in Apple Developer Program ($99/year)
- Create an App ID (Bundle Identifier, e.g.,
com.company.app) - Configure capabilities (Push Notifications, Associated Domains, etc.)
| Certificate | Purpose |
|---|---|
| Apple Development | Debug builds on registered devices |
| Apple Distribution | App Store and Ad Hoc releases |
| Push Notification | APNs authentication |
A provisioning profile links:
- App ID
- Distribution certificate
- Device list (development/ad hoc) or App Store distribution
# Modern approach: Xcode Automatic Signing
# Xcode → Target → Signing & Capabilities → Automatically manage signing
# Manual / CI approach with Fastlane Match
# Stores certs and profiles in encrypted git repo# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Build and upload to App Store Connect"
lane :release do
setup_ci if ENV['CI']
match(
type: "appstore",
app_identifier: "com.company.myapp",
readonly: true
)
increment_build_number(xcodeproj: "MyApp.xcodeproj")
build_app(
scheme: "MyApp",
export_method: "app-store",
configuration: "Release"
)
upload_to_app_store(
skip_metadata: false,
submit_for_review: false,
precheck_include_in_app_purchases: false
)
end
end- Create app record in App Store Connect
- Upload build via Xcode, Transporter, or Fastlane
- Fill metadata: description, screenshots, privacy policy URL
- Complete App Privacy questionnaire
- Submit for review
- Release manually or automatically after approval
Use TestFlight for beta distribution before public release. Internal testers (up to 100) get builds immediately; external testers require brief Beta App Review.
Google Play requires AAB format (not APK) for new apps. Google generates optimized APKs per device configuration (screen density, CPU architecture, language).
# Generate release AAB
cd android
./gradlew bundleRelease
# Output: android/app/build/outputs/bundle/release/app-release.aab// android/app/build.gradle
android {
signingConfigs {
release {
if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
storeFile file(MYAPP_UPLOAD_STORE_FILE)
storePassword MYAPP_UPLOAD_STORE_PASSWORD
keyAlias MYAPP_UPLOAD_KEY_ALIAS
keyPassword MYAPP_UPLOAD_KEY_PASSWORD
}
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}# android/gradle.properties (never commit passwords — use CI secrets)
MYAPP_UPLOAD_STORE_FILE=my-release-key.keystore
MYAPP_UPLOAD_KEY_ALIAS=my-key-alias
MYAPP_UPLOAD_STORE_PASSWORD=*****
MYAPP_UPLOAD_KEY_PASSWORD=*****keytool -genkeypair -v \
-storetype PKCS12 \
-keystore my-release-key.keystore \
-alias my-key-alias \
-keyalg RSA \
-keysize 2048 \
-validity 10000Critical: Back up your keystore. Google Play App Signing can recover upload key loss, but losing both is catastrophic.
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
desc "Deploy to Google Play internal track"
lane :internal do
gradle(task: "clean bundleRelease")
upload_to_play_store(
track: "internal",
aab: "app/build/outputs/bundle/release/app-release.aab",
json_key: "play-store-credentials.json"
)
end
end| Track | Audience | Purpose |
|---|---|---|
| Internal | Up to 100 testers | Fast CI validation |
| Closed | Invited groups | Beta testing |
| Open | Public opt-in | Large beta |
| Production | All users | Live release |
Microsoft CodePush (via App Center) delivers JavaScript bundle updates without store review.
| Allowed | Not Allowed |
|---|---|
| JS bundle changes | Native module additions |
| Image/asset updates | Permission changes |
| Bug fixes in logic | SDK version bumps |
| UI layout changes | New native dependencies |
npm install react-native-code-push
appcenter apps create -d MyApp-iOS -o iOS -p React-Native
appcenter apps create -d MyApp-Android -o Android -p React-Native
appcenter codepush deployment add -a MyOrg/MyApp-iOS Staging
appcenter codepush deployment add -a MyOrg/MyApp-iOS Production// App.tsx
import codePush from 'react-native-code-push';
function App() {
return <RootNavigator />;
}
const codePushOptions = {
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
installMode: codePush.InstallMode.ON_NEXT_RESTART,
mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
};
export default codePush(codePushOptions)(App);# Release to Staging
appcenter codepush release-react \
-a MyOrg/MyApp-iOS \
-d Staging \
--description "Fix checkout bug"
# Promote Staging → Production after validation
appcenter codepush promotion -a MyOrg/MyApp-iOS -s Staging -d Production- Always test on Staging deployment first
- Use mandatory flag for critical security fixes
- Maintain compatibility between native binary and JS bundle versions
- Log CodePush version in crash reports for debugging
Expo projects use EAS Update instead of CodePush for OTA delivery.
npm install -g eas-cli
eas login
eas update:configure// app.json
{
"expo": {
"runtimeVersion": {
"policy": "appVersion"
},
"updates": {
"url": "https://u.expo.dev/your-project-id"
}
}
}# Publish an update to a channel
eas update --branch production --message "Fix login redirect"
# Build native binary separately
eas build --platform all --profile productionruntimeVersion ensures OTA updates only apply to compatible native binaries. Mismatch prevents crashes from missing native modules.
| Policy | Behavior |
|---|---|
"appVersion" |
Tied to version in app.json |
"nativeVersion" |
Tied to build number |
| Custom string | Manual control |
MAJOR.MINOR.PATCH
2 . 4 . 1
MAJOR — Breaking changes
MINOR — New features (backward compatible)
PATCH — Bug fixes
| Platform | User-Facing | Internal Build |
|---|---|---|
| iOS | CFBundleShortVersionString (1.2.3) |
CFBundleVersion (42) |
| Android | versionName (1.2.3) |
versionCode (42) |
// package.json
{ "version": "1.2.3" }# Fastlane — sync versions
increment_version_number(version_number: "1.2.3")
increment_build_number// android/app/build.gradle
defaultConfig {
versionCode 42
versionName "1.2.3"
}- Increment build number on every store upload (even same version)
- Never reuse version codes on Play Store
- Tag git commits with release versions
- Changelog per release for support and rollback decisions
App Store Connect offers phased release over 7 days:
- Day 1: ~1% of users
- Gradually increases to 100% by day 7
- Can pause at any point
Play Console allows percentage rollout (1% → 5% → 20% → 50% → 100%):
Production release → Start at 10% → Monitor crash rate → Increase to 50% → Full rollout
Monitor during rollout:
- Crash-free rate (Firebase Crashlytics, Sentry)
- ANR rate (Android)
- Key business metrics (conversion, retention)
- Store reviews and support tickets
| Scenario | Action |
|---|---|
| JS bug | Push OTA fix or rollback CodePush/EAS deployment |
| Native crash | Halt rollout; submit hotfix binary |
| Critical issue | Pull app from sale (last resort) |
## Pre-Release Checklist
### Code Quality
- [ ] All tests pass in CI
- [ ] No debug logs or dev API URLs
- [ ] Feature flags configured for production
- [ ] Error reporting (Sentry) configured for release
### iOS
- [ ] Version and build number incremented
- [ ] Provisioning profile valid
- [ ] Privacy manifest (PrivacyInfo.xcprivacy) complete
- [ ] App Store screenshots and metadata updated
- [ ] TestFlight beta tested
### Android
- [ ] versionCode incremented
- [ ] Release keystore configured in CI
- [ ] ProGuard rules tested (no runtime crashes)
- [ ] Play Store listing updated
- [ ] Internal track tested
### OTA (if applicable)
- [ ] Runtime version matches native binary
- [ ] Staging deployment validated
- [ ] Rollback plan documentedAnswer:
- Apple Developer account — Enroll and create App ID matching bundle identifier.
- Certificates & profiles — Create Distribution certificate and App Store provisioning profile (or use Xcode automatic signing / Fastlane Match).
- Configure release build — Set version (
CFBundleShortVersionString) and build number (CFBundleVersion) in Xcode or via Fastlane. - Archive & upload — Build release scheme, archive in Xcode, upload to App Store Connect via Transporter or Fastlane
upload_to_app_store. - App Store Connect — Select build, complete metadata, screenshots, privacy questionnaire, and export compliance.
- TestFlight — Optional beta testing with internal/external testers.
- Submit for review — Apple review takes 24-48 hours typically.
- Release — Manual release or automatic after approval; optionally enable phased release.
React Native-specific: ensure Hermes bytecode, native modules, and privacy manifests are correctly configured before archiving.
Answer:
AAB is Google's publishing format that contains compiled app resources and native libraries. Google Play's servers generate optimized APKs for each device configuration — resulting in smaller downloads than universal APKs.
Signing steps:
- Generate upload keystore with
keytool - Configure
signingConfigs.releaseinandroid/app/build.gradle - Store credentials in
gradle.propertiesor CI environment variables (never in git) - Run
./gradlew bundleReleaseto produce signed AAB - Upload to Play Console
Play App Signing: Google manages the app signing key; you upload with an upload key. This enables key recovery if upload key is lost.
Enable ProGuard/R8 for release builds to shrink and obfuscate native Java/Kotlin code.
Answer:
OTA (Over-The-Air) updates deliver new JavaScript bundles and assets to users without App Store / Play Store review.
CodePush (App Center) and EAS Update (Expo) are popular solutions.
Can update:
- JavaScript logic and React components
- Images and static assets bundled with JS
- Bug fixes and feature toggles
Cannot update:
- Native code changes (new native modules, SDK upgrades)
- Permission changes in Info.plist / AndroidManifest
- App icon or splash screen (native assets)
Apple's guidelines require OTA updates not to significantly change app purpose. Major feature changes should go through store review.
Always use deployment channels (Staging → Production) and set runtime version compatibility to prevent mismatched native/JS bundles.
Answer:
| Aspect | CodePush | EAS Update |
|---|---|---|
| Ecosystem | App Center / Microsoft | Expo |
| Project type | RN CLI and Expo | Primarily Expo (config plugins for bare) |
| Configuration | Manual native setup | eas update:configure |
| Channels | Deployments (Staging/Prod) | Branches and channels |
| Runtime compat | Manual target binary version | runtimeVersion policy in app.json |
| Integration | Separate from build pipeline | Unified with EAS Build |
EAS Update integrates with Expo's build system — same project ID, unified dashboard. CodePush works with any React Native project but requires App Center setup.
Both serve the same purpose: fast JS-only updates between store releases.
Answer:
Use semantic versioning for user-facing version (1.4.2) synchronized across platforms via package.json and release automation.
Build numbers (iOS CFBundleVersion, Android versionCode):
- Increment on every store upload
- Never decrement or reuse (Play Store enforces this)
- Can automate in CI with Fastlane
increment_build_number
Git tagging: Tag releases as v1.4.2 for traceability.
OTA compatibility: Match CodePush target binary version or EAS runtimeVersion to the native build that JS update requires.
Branch strategy: release/1.4.x branches for hotfixes; merge back to main after release.
Answer:
Staged rollout releases a new version to a percentage of users before full deployment.
iOS: Phased release over 7 days (automatic 1% → 100%). Android: Manual percentage control (e.g., 5% → 20% → 100%).
Why it matters:
- Catches crashes affecting only specific devices/OS versions
- Limits blast radius of undiscovered bugs
- Allows monitoring crash-free rate before full release
- Can halt rollout if metrics degrade
Best practice: Combine with crash monitoring (Crashlytics/Sentry), key funnel metrics, and a documented rollback plan. For JS-only issues, OTA fix can reach 100% of users on the current binary without waiting for staged native rollout to complete.
Answer:
A provisioning profile is Apple's mechanism to authorize an app to run on devices or be distributed through the App Store. It binds together:
- App ID — Bundle identifier and enabled capabilities
- Certificate — Proves developer identity (Development or Distribution)
- Devices — Registered UDIDs (development/ad hoc only; not needed for App Store)
Without a valid profile, iOS refuses to install or launch the app.
Types:
- Development — Debug on registered devices
- Ad Hoc — Internal distribution to registered devices
- App Store — Distribution via App Store / TestFlight
Modern workflows use Xcode Automatic Signing or Fastlane Match (stores profiles in encrypted git repo for CI). Profiles expire annually — CI pipelines must handle renewal.
Answer:
Fastlane automates repetitive release tasks:
iOS lane:
match— Sync certificates and profilesincrement_build_number— Bump buildbuild_app— Archive and export IPAupload_to_app_store— Upload to App Store Connectdeliver— Upload metadata and screenshots
Android lane:
gradle(task: "bundleRelease")— Build AABupload_to_play_store— Upload to Play Console track
CI integration (GitHub Actions example):
- name: iOS Release
run: cd ios && fastlane release
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_KEY }}Combine with:
- Automated version bumping from conventional commits
- TestFlight / internal track on every main branch merge
- Production release triggered by git tag
Answer:
Code & config:
- Remove debug code, dev URLs, and console.logs
- Verify environment variables point to production
- Confirm crash reporting and analytics use production keys
- All CI tests pass including E2E on release build
iOS:
- Valid distribution certificate and provisioning profile
- Privacy manifest and App Tracking Transparency if applicable
- Version/build incremented; release notes prepared
- TestFlight validation completed
Android:
- versionCode incremented; release signing configured
- ProGuard tested — no reflection-related crashes
- 64-bit libraries included
- Internal track smoke test passed
Store listing:
- Screenshots for required device sizes
- Privacy policy URL current
- Release notes written
Post-release:
- Monitor crash-free rate for 24-48 hours
- Staged rollout before 100%
- OTA rollback plan documented
Previous: Architecture Patterns
Next: Security