React Native on Google Play
Publishing an Expo/React Native app to Google Play with a local AAB
An Expo/React Native app can reach Google Play without EAS, but a local AAB only earns trust when the release inputs are explicit and repeatable.
Publishing an Expo/React Native app to Google Play looks simple until the first real upload: generate an AAB, upload it to Play Console, and wait for the internal track to become available.
In practice, the AAB is only the package. What decides whether the build is ready is everything around it: signing, Firebase, Sentry, ads, production configuration, versioning, and installation through Google Play itself.
This article summarizes a local release flow used in a real app, Daily Sudoku: Offline Puzzle, without relying on EAS Build.
What a local build needs to prove
A store AAB should not be just “a file that compiles”. It needs to prove that the app, once installed from Google Play, uses the correct production services.
For this app, the minimum checklist was:
- The correct Android package:
com.sunstoneapps.dailysudoku. - A new
versionCodefor each upload. - A valid local upload key.
- Play App Signing enabled.
- Firebase pointing to the official project.
- Sentry initialized with matching
releaseanddist. - Source maps uploaded to Sentry.
- AdMob using production IDs.
- RevenueCat using the real Android SDK key.
- Installation coming from Google Play, not sideload.
That last item matters. A locally installed build can work while still not proving that Google Play delivered the right split APK with the expected signing, resources, and configuration.
The build command
The local flow was wrapped in a root-level helper command:
pnpm sudoku:aab:clean
Under the hood, the command does the work that otherwise turns into scattered manual steps:
load apps/sudoku/.env
generate typed production config
generate google-services.json
validate required Sentry env vars
validate real AdMob IDs
validate local upload signing
run expo prebuild --platform android --no-install
apply required native build.gradle adjustments
run gradlew bundleRelease --rerun-tasks
upload source maps to Sentry
generate app-release.aab
The --rerun-tasks part became mandatory for store AABs after a real cache bug. For local iteration, pnpm sudoku:aab
remains useful. For a file headed to Play Console, the clean version is safer.
What should stay out of Git
The build needs secrets and files that should not be versioned:
- Upload key path.
- Upload key alias.
- Keystore password.
- Key password.
- Sentry token.
- API keys loaded from the local environment.
A local .env ignored by Git is enough for individual development. For a team or CI, those values should move into a secrets
manager.
Do not confuse secrets with public configuration. Some IDs, such as public SDK app IDs, can live in versioned YAML when they are not secret. Passwords, tokens, and key files stay out of the repository.
The Play Console upload needs evidence too
Once the AAB exists, there is still one step that deserves the same discipline as the build: uploading it to the right track.
For the Daily Sudoku internal test, Play Console created a draft release, accepted app-release.aab, optimized the bundle for
distribution, and only then allowed the final review.
The useful evidence was not “the upload finished”. It was the internal track summary saying:
Última versão: 8 (0.1.0)
That small check prevents a common release mistake in small teams: treating an uploaded file as an active release. In Play Console, you still need to fill the release notes, review warnings, confirm publication, and return to the track page to see which version is active.
One non-blocking warning appeared during that flow: no deobfuscation file was attached. For a React Native/Expo app with Sentry source maps uploaded, that did not block the internal track. It should not be forgotten, though. If the project starts leaning harder on R8/ProGuard to reduce size or protect Java/Kotlin bytecode, the mapping file belongs in the release checklist.
Firebase: validate the app installed from Play
On Android, Firebase depends on the correct google-services.json. For a local build, that file can be generated from .env
instead of edited manually.
After installing the build from Google Play, confirm the app ID through adb logcat or DebugView:
gmp_app_id: 1:832961368149:android:043d6431c9c975f7fbc4dc
Then confirm real events:
app_opened
shop_viewed
session_start
The common mistake is validating a local build while the Play-delivered app still points to another Firebase project. The
evidence needs to come from a build installed by com.android.vending.
Sentry: release and dist need to match the build
For React Native, uploading source maps to Sentry is not enough. The runtime needs to initialize with the same release and
dist that were uploaded.
The expected format was:
com.sunstoneapps.dailysudoku@0.1.0+4
dist=4
At startup, the expected log looked like this:
[crash-reporting] initialize {
"enabled": true,
"hasDsn": true,
"environment": "production",
"release": "com.sunstoneapps.dailysudoku@0.1.0+4",
"dist": "4"
}
If Sentry receives events under an old release, you do not have an observability problem. You have a build problem.
AdMob: separate no-fill from broken integration
In real tests, AdMob can return no-fill. That means missing inventory, not necessarily a broken integration.
The first smoke test did not look for perfect ads. It looked for initialization and request signals:
ad_query(_aq)
That event shows that the SDK attempted an ad request. Visual validation for banners, interstitials, and UMP still needs to
happen before widening the test, but ad_query separates “the SDK never initialized” from “there was no inventory now”.
RevenueCat: an API key is not a product
After configuring the public Android RevenueCat key, the old error disappeared:
RevenueCat API key is missing.
But another error appeared:
ConfigurationError: there are no Play Store products registered in the RevenueCat dashboard for your offerings
That second error is better. It means the SDK is configured, but the commercial setup does not exist yet: Play Billing products, offerings, and the Google Play service account connection in RevenueCat.
Final checklist before expanding internal testing
Before inviting more testers, I would validate:
versionCodeinstalled from Google Play.installerPackageName=com.android.vending.- Firebase using the official project.
- Sentry with the correct
releaseanddist. - Source maps processed in Sentry.
- Minimum analytics events.
- No crash on cold start.
- AdMob emitting at least one request.
- UMP not blocking the experience.
- RevenueCat without API key errors.
- Products and offerings configured before testing real purchases.
Local does not mean informal
A local AAB build can be perfectly valid for Google Play, but it needs the same discipline as any hosted pipeline. The machine must have the expected Java version, Android SDK, signing inputs, environment variables, and generated config. If those inputs are implicit, the release is not reproducible.
For small teams, the practical answer is a scripted release command that fails loudly when a required value is missing. The command should generate config, build the bundle, keep the output path predictable, and avoid debug-only shortcuts. A release built by memory is easy to mistrust later.
What should be checked after the AAB exists
Generating the file is only the first half. Before uploading, inspect the things that commonly diverge between local and store behavior:
- package name and version code;
- signing config and Play App Signing expectations;
- production API keys and public IDs;
- Sentry release/dist or equivalent crash reporting metadata;
- ad, billing, analytics, and consent providers for the selected environment.
A local build is useful because it is fast to repeat. Use that advantage to run a small release checklist every time instead of relying on a single successful upload.
When EAS or CI becomes worth it
Local builds are a good starting point when the app is young and the release owner understands the environment. They become fragile when multiple people need to release, when credentials move between machines, or when auditability matters more than speed.
That is the point where EAS, CI, or another controlled build environment becomes less about convenience and more about reducing variance. The goal is not to avoid local builds forever. The goal is to know when local state has become a hidden dependency.
Keep release and debug installs separate
A debug install is useful for development, but it is a poor substitute for release validation. It can use a different signature, different runtime settings, different network behavior, and different integration paths for billing or ads. If the goal is to validate store behavior, install the release artifact.
That distinction becomes important when several devices are connected. A script should install the same APK or AAB-derived artifact consistently, and any device-specific override should be explicit. Otherwise a tester can accidentally validate the wrong binary and make a real release look safer than it is.
Conclusion
A local build is not inferior to a managed build, but it requires discipline. The gain is control: you know exactly which env was used, which file was generated, which release was sent to Sentry, and which AAB reached Play Console.
The cost is reproducibility. If the sequence depends on hidden manual steps, it will fail at the worst time: when you are already comparing logs from an app installed through Google Play.