Intro
Production is a different game than development. This checklist gives you a practical, field-tested path to harden a React Native app for release, secure data and payments, add monitoring, improve performance, plan releases, and recover from incidents across Android, iOS, and Expo.
Workflow Overview
Use a simple, explicit flow to reduce rework and surprises:
- Plan: define target devices, OS versions, and success metrics (crash-free sessions > 99.5%, cold start p50 < 2s, API error rate < 0.5%, payment success rate > 99%).
- Configure: set release flags (Hermes, minification, resource shrinking, inline requires), versioning, and environment separation.
- Secure: protect secrets, lock network transport, and review permissions.
- Instrument: crash reporting, JS error handlers, performance and business metrics.
- Test: smoke tests on low-end and high-end devices; payment sandbox; offline and flaky network.
- Release gradually: internal testers, staged rollouts, watch dashboards.
- Operate: monitor, on-call ownership, backups, and regular upgrades.
Configuration and Build Flags
Harden release builds on both platforms.
- General
- Set proper app ID/bundle ID (e.g.,
com.company.appname) and semver (e.g.,1.7.0) with incrementing build numbers per store upload. - Disable debug-only libraries and logs in release.
- Separate endpoints and keys for dev, staging, and production using
app.config.jsor.envfiles per environment.
- Metro config: speed up startup by deferring requires
// metro.config.js
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: { inlineRequires: true }
})
}
};
- Android (Gradle)
- Enable Hermes and R8, shrink resources, split ABIs to reduce APK size.
// android/app/build.gradle
project.ext.react = [ enableHermes: true ]
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
splits {
abi { enable true; reset(); include 'armeabi-v7a','arm64-v8a','x86_64'; universalApk false }
}
}
- Strip logs in release via Babel plugin:
// babel.config.js
plugins: [
['transform-remove-console', { exclude: ['error', 'warn'] }]
]
- Set network security config; avoid cleartext unless required and scoped. Example
res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.example.com</domain>
</domain-config>
</network-security-config>
- Configure Play App Signing and secure keystores.
- iOS (Xcode)
- Enable Hermes in Podfile if using Hermes, set Release flags (dead code stripping, bitcode off for current toolchains, optimization level
-Os). - Enforce ATS (HTTPS) with exceptions only if absolutely required and documented in
Info.plist. - Configure correct provisioning and signing for Release.
Secrets and Security
- Never embed long-lived secrets in the app. Keep private keys on the server.
- Store access tokens in platform secure storage:
- iOS: Keychain (restrict access group and accessibility level as needed, e.g.,
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly). - Android: EncryptedSharedPreferences or Keystore-backed storage.
- Avoid storing secrets in AsyncStorage.
- Payments
- Stripe: keep publishable key in app, but create ephemeral keys server-side. Do not store customer secrets on-device.
- Google Play Billing: validate purchases on a secure server; do not trust client-only checks.
- Permissions: request only what you need; provide in-app rationale; handle denial gracefully.
- Transport: enforce HTTPS/TLS, pin domains if you control the backend; set sane timeouts (e.g., 10s connect, 30s read).
Monitoring and Crash Reporting
- Add a crash reporter (native + JS) and performance monitoring (e.g., Sentry, Datadog, Bugsnag).
- Initialize early in app startup and include release, build number, device, and user ID or session ID (respect privacy and consent).
- Capture unhandled JS errors and rejections:
import { setJSExceptionHandler, setNativeExceptionHandler } from 'react-native-exception-handler';
setJSExceptionHandler((e, isFatal) => {
// send to your crash service
Sentry.captureException(e);
}, true);
setNativeExceptionHandler(errorString => {
// send native crash info
Sentry.captureMessage(errorString);
});
- Track: app start time (cold/warm), UI freezes (> 16ms frame drops), network failures, payment flow outcomes, and screen transition timings.
- Alerts: set thresholds for crash-free sessions (< 99.5%), error spikes (> 5% in 5 min), payment drop-offs (> 2% deviation).
Performance and Startup
- Use Hermes for faster startup and lower memory (typically 20-30% faster TTI on low-end devices).
- Enable
inlineRequiresand lazy-load heavy screens and modules. - Avoid expensive work on startup; defer analytics and non-critical fetches until after first paint.
- Optimize images: WebP/AVIF where supported, correct sizes, caching via
cache: 'force-cache'headers. - Use
FlatList/SectionListwithkeyExtractor,getItemLayoutwhen possible; avoid re-render storms (memoize props,useCallback/useMemoappropriately).
Networking and API Resilience
- Set timeouts and retries with exponential backoff and jitter.
import axios from 'axios';
const api = axios.create({ baseURL: 'https://api.example.com', timeout: 10000 });
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const shouldRetry = (error) => !error.response || error.response.status >= 500;
api.interceptors.response.use(undefined, async (error) => {
const cfg = error.config;
cfg.__retryCount = cfg.__retryCount || 0;
if (cfg.__retryCount < 3 && shouldRetry(error)) {
cfg.__retryCount++;
const backoff = Math.min(1000 * 2 ** cfg.__retryCount, 8000);
const jitter = Math.random() * 500;
await delay(backoff + jitter);
return api(cfg);
}
return Promise.reject(error);
});
- Use idempotency keys for write operations (especially for payments).
- Offline support: cache reads, queue writes, and reconcile on connectivity restore (use
NetInfoto detect status). - Validate SSL and enforce ATS on iOS. Avoid cleartext traffic; if unavoidable, scope hosts and paths tightly.
Payments: Play Billing and Stripe
- Google Play Billing (in-app purchases or subscriptions)
- Use the latest BillingClient (v6+).
- Handle purchase states:
Purchased,Pending, andUnspecified. Show UI for pending (e.g., cash-based payments in some regions like OXXO in Mexico or Boleto in Brazil). - Always acknowledge purchases within 3 days (consumables) or before granting entitlement (subscriptions).
- Verify purchase tokens server-side against Play Developer API.
- Support subscription upgrades/downgrades and proration modes (
IMMEDIATE_WITH_TIME_PRORATION, etc.). - Test with license test accounts and sandbox products.
- Stripe (cards, wallets)
- Prefer
PaymentSheetfor a secure, standardized flow. - Generate ephemeral keys on your server; never embed secret keys in the app.
- Handle 3DS authentication, app switching, and
Activity/UIViewControllerlifecycle. - Use idempotency keys for payment intents to avoid duplicates on retries.
- Log payment attempt outcomes and correlate with backend logs.
Expo Considerations
- Choose managed vs bare based on native needs.
- EAS Build
- Define build profiles for development, preview, and production.
- Use runtime versions and channels for safe update segmentation.
- Updates
- Use OTA updates for small JS/asset fixes that comply with store policies.
- Any native module or permission changes require a new store build.
- Config
- Keep
app.json/app.configin sync with store metadata (icons, permissions, deep links, schemes).
Release Management: Android and iOS
- Android
- Use internal testing track for smoke tests, closed testing for broader QA, then staged rollout to production (e.g., 5%, 25%, 50%, 100%).
- Enable Play App Signing and integrity protections.
- Keep a rollback plan: halt rollout or roll back to a prior release within Play Console.
- iOS
- Use TestFlight for internal and external testers (up to 10k external).
- Consider phased release for production (1-day, 7-day, 30-day options).
- Provide compliance metadata and accurate permission descriptions in App Store Connect.
- Versioning and metadata
- Use semantic versioning for marketing version and increment build numbers per upload.
- Maintain clear changelogs and update notes.
Data, Storage, and Backups
- Classify data: credentials/tokens, user content, cached API data, analytics.
- Store sensitive data in secure storage; never in plaintext files or AsyncStorage.
- Migrations: version your schema for SQLite or key-value stores and test upgrades and downgrades.
- Android backups
- Control Auto Backup with a
fullBackupContentXML to exclude sensitive paths (e.g.,shared_prefs,databases,files/keys). - iOS backups
- Mark files that should not be backed up with the
NSURLIsExcludedFromBackupKeyattribute when appropriate. - Keychain items may migrate across device restores; plan token invalidation accordingly (e.g., bind tokens to device ID or rotate on first launch after restore).
- Backend coordination: ensure server-side backups, retention, and restore drills align with client expectations.
Maintenance and SLOs
- Define targets
- Crash-free sessions: > 99.5% (Google Play "Excellent" threshold).
- Cold start p50 < 2s, p90 < 4s on median devices.
- API error rate < 0.5%, payment success rate > 99%.
- Reviews
- Weekly monitoring review: crashes, ANRs, perf regressions.
- Monthly dependency review: security advisories and compatibility (React Native, Gradle, Xcode, key libraries).
- Access and keys
- Rotate credentials and cleanup unused access quarterly.
Upgrades and OS Compatibility
- Plan quarterly React Native and library upgrades; avoid large version jumps (e.g., 0.72 -> 0.74, not 0.70 -> 0.74).
- Track Android Gradle Plugin, Kotlin, Java, Xcode, Swift, and iOS/Android SDK targets in a
VERSIONS.mdfile. - Test on the latest OS betas ahead of public release; fix deprecations early (e.g., Android 14 foreground service types, iOS 17 privacy manifests).
- Pin toolchain versions in
gradle.propertiesandPackage.swift/xcodeprojto reduce unexpected build breaks.
Incident Response and Rollbacks
- Feature flags and remote kill switches (e.g., LaunchDarkly, Firebase Remote Config) to disable risky features without store updates.
- OTA rollback strategy (where allowed) for JS-only fixes via Expo Updates or CodePush.
- Store controls
- Android: halt or roll back staged rollouts in Play Console.
- iOS: remove from sale or submit expedited review for hotfixes when necessary.
- Runbooks
- Define steps for payment outage, API outage, and crash spike events.
- Include contact points, log links, dashboards, and decision trees (e.g., "if crash-free sessions < 99% for 15 min -> alert on-call -> check Sentry -> if new release -> halt rollout\"}).
Local Pilot Plan
Start small so you can verify quickly and safely.
Scope
- Enable Hermes and
inlineRequires. - Add crash reporting and capture unhandled JS errors.
- Wrap one critical API call with timeouts and backoff, plus idempotency for writes.
Verification
- Measure cold start time on two devices (e.g., Pixel 6a and Pixel 8) and compare before/after.
- Trigger a handled JS error to confirm reporting appears in dashboard within 60 seconds.
- Simulate network failures (airplane mode, throttling) and confirm retries and user messaging.
Rollout
- Ship to an internal test track (Android) or TestFlight (iOS) with 10-20 testers.
- Success criteria: no new crashes from pilot changes, p50 cold start improved > 10%, and API error handling verified.
Conclusion
A reliable release comes from repeatable operations: hardened builds, secured secrets, robust monitoring, tuned performance, resilient networking, careful payments handling, and disciplined releases. Start with a narrow pilot, verify improvements, then expand coverage to the rest of the app. Keep upgrades and maintenance on a predictable cadence, and practice rollback paths before you need them.