Build a release app bundle
React Native packs your JavaScript into the native Android build, so the release .aab comes out of Gradle. Set up signing first (next section), then:
Check the package name
applicationIdinandroid/app/build.gradleis permanent once you upload to Google Play. Make sure it's the one you want.Build
From the project root run
npx react-native build-android --mode=release, or call Gradle directly as below. Both run thebundleReleasetask and bundle the JavaScript into the app.Find the file
The bundle is written to
android/app/build/outputs/bundle/release/app-release.aab.Test the release build
Uninstall the debug app, then install the release variant on a real device with the second command below. Release-only crashes show up here, not in Play Console.
cd android
./gradlew bundleReleasenpm run android -- --mode="release"Don't set org.gradle.configureondemand=true in gradle.properties. It can make the release build skip bundling JavaScript and assets, and testers get an app that can't start.
Upload key and Play App Signing
Google Play requires Play App Signing for new apps: Google keeps the app signing key, and you sign uploads with an upload key. Generate one with keytool, which ships with the JDK:
keytool -genkeypair -v -storetype PKCS12 -keystore my-upload-key.keystore \
-alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000Move the file to android/app/ and put the credentials in ~/.gradle/gradle.properties so they never end up in git:
MYAPP_UPLOAD_STORE_FILE=my-upload-key.keystore
MYAPP_UPLOAD_KEY_ALIAS=my-key-alias
MYAPP_UPLOAD_STORE_PASSWORD=*****
MYAPP_UPLOAD_KEY_PASSWORD=*****Then add a release signing config in android/app/build.gradle and use it in the release build type. The template's release build type signs with the debug key until you change this:
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
}
}
}Version code and target API level
Each upload needs a higher versionCode than everything you have uploaded before. In a bare project it lives in android/app/build.gradle; versionName is the label users see:
android {
defaultConfig {
versionCode 2
versionName "1.0.1"
}
}Since 31 August 2026, new apps and updates must target Android 16 (API level 36). The SDK levels sit in the ext block of android/build.gradle (compileSdkVersion, targetSdkVersion). New React Native releases raise them. On an older version, upgrading React Native is usually safer than raising targetSdkVersion alone, because your native libraries have to be compatible too.
React Native pitfalls
- Debug-signed bundle. Forget to switch the release build type to
signingConfigs.releaseand Play Console rejects the upload as debug-signed. - R8/ProGuard surprises. With
enableProguardInReleaseBuilds = true, libraries that rely on reflection can crash only in release. Add their keep rules toandroid/app/proguard-rules.proand test the release build on a device. - Unreadable crash reports. Hermes, the default engine, runs precompiled bytecode, so release stack traces point into the compiled bundle. Keep the source maps from each build you upload so you can symbolicate what testers send.
- The app looks for Metro. A release build that tries to reach the development server means the JavaScript wasn't bundled. Check the
configureondemandsetting and build again. - Native modules and 16 KB pages. Libraries with native
.sofiles must support 16 KB memory pages. Play Console warns about it, and from 1 February 2027 updates without support can't be released. Update them before the test starts. - Lost keystore. Back up the keystore and its passwords. You can request an upload key reset in Play Console, but it takes time you don't want to lose in the middle of a test.
Symbolicate crashes from testers
A JavaScript crash in a release build reaches you as a Hermes bytecode position such as p@1:132161, which says nothing on its own. React Native generates source maps for Android release builds by default; what matters is keeping the right map for every bundle you upload.
Archive the source map
After each release build, copy the map from
android/app/build/generated/sourcemaps/react/release/and label it with the version code. It must match the exact commit of the crashing build: even small code changes move the offsets.Run metro-symbolicate
Save the tester's stack trace to a file, or read it straight from
adb logcat, and pipe it throughmetro-symbolicatewith that map. It turns bytecode offsets back into file names, lines and function names.Check native frames in Play Console
Crashes in Java or Kotlin code from native modules appear under Monitor and improve → Android vitals → Crashes & ANRs. With R8 enabled, the mapping file inside the
.aabmakes them readable without an extra upload.
# From a saved stack trace
npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map < stacktrace.txt
# From a connected device
adb logcat -d | npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.mapIf metro-symbolicate exits at once without output, it didn't get any input: pass the trace through a pipe or a redirect, not by typing it into the terminal.
Play Console in short: from closed test to production
Once your React Native bundle is built and signed, the rest happens in Play Console and works the same for every framework. This is the checklist; the full Play Console walkthrough covers every menu and screen.
- Finish app setup on the Dashboard: privacy policy, app access, ads, content rating, target audience, data safety and store listing.
- Create a closed testing release under Test and release → Testing → Closed testing: choose your testers' countries, upload the
.aaband send it for review. - Add testers with a Google Group on the Testers tab and share the opt-in link. It works once the track shows Published.
- Keep at least 12 testers opted in for 14 days in a row. Internal testing doesn't count, and someone who leaves and rejoins starts their 14 days again.
- Ship at least one update during the test and write down what testers found and what you changed.
- Apply for production from the Dashboard and answer the three sections about your test, your app and your readiness. The decision is Google's; nobody can guarantee approval.
Plan for drop-outs: recruit more than 12 testers so one person leaving doesn't push back your timeline. See the full Play Console steps.
The full Play Console walkthrough
Closed track, Google Group and opt-in link, the 12 testers / 14 days rule and the production application, step by step on one page.
Ship updates during the test
For React Native: raise versionCode (and versionName), build again and upload the new bundle to the same closed track.
cd android
./gradlew bundleReleaseUninstall the debug app before installing the release variant with npm run android -- --mode="release"; Android won't replace an app signed with a different key. Repeat that check before every upload, because release-only problems such as missing keep rules never show up in debug.
Over-the-air JavaScript updates are a gray area many teams ask about. Google Play's Device and Network Abuse policy says an app may not update itself outside Google Play, but the restriction doesn't apply to code that runs in a virtual machine or an interpreter, such as JavaScript. Native code, new permissions and native modules always need a new bundle through Google Play.
- Keep the closed track as the main path. A new
versionCodeon the closed track reaches every opted-in tester through Google Play and shows up in your release history. - Keep native dependencies still. Upgrading React Native or a native module mid-test changes far more than the bug you're fixing. Do it before the test or after production access.
- Keep one map per release. Every rebuild produces a new source map. Store it with the version code, or the next crash report from that version can't be decoded.
Frequently asked questions
Do I need Hermes for Google Play?
No, but it's React Native's default engine and the one most apps ship with. It precompiles JavaScript to bytecode, which speeds up startup and is also why release stack traces need a source map to be readable.
Which file do I upload to Play Console?
The bundle at android/app/build/outputs/bundle/release/app-release.aab. npx react-native build-android --mode=release runs Gradle's bundleRelease task and packs all of your JavaScript into it.
Why does my layout slide under the status bar after raising targetSdk?
Apps that target Android 15 (API level 35) or higher are drawn edge to edge. react-native-safe-area-context already handles this; without it, add safe-area insets to every screen before testers see it.
Does React Native support 16 KB memory pages?
Yes, since React Native 0.77. Older versions, and native modules that ship their own .so files, may not. Upgrade before the test starts rather than in the middle of it.
Sources
Technical details were checked against the official documentation below. Menus and defaults change between versions; when in doubt, the official page wins.
- React Native: Publishing to Google Play Store
- Android Developers: Sign your app
- Android Developers: Target API level requirements
- Android Developers: Support 16 KB page sizes
- React Native: Debugging release builds
- React Native 0.77 release notes (16 KB pages, edge-to-edge)
- Play Policy: Device and Network Abuse
- Play Console Help: App testing requirements for new personal developer accounts
- Play Console Help: Set up an open, closed, or internal test
- Play Console Help: Deobfuscate or symbolicate crash stack traces
New to the 12 testers rule?
The general guide explains Google's requirement, how a TestBunny campaign runs day by day and what we can and can't guarantee.
Read the general guide