React Native beta distribution without the store

A React Native app leaves your machine as a plain native binary — an .apk on Android, an .ipa on iOS. Sharing a build with testers is the same job as sharing any native app, with one trap this stack invented for itself: handing someone a debug build that still expects the Metro server on your laptop.

Here is the whole path for both platforms — the exact build commands, where the artifact lands, and the two errors that reliably eat an afternoon. No Play Store track, no TestFlight review, no tester accounts: upload the binary, send a link.

Android: from assembleRelease to an install link

  1. Build the release APK. From your project root:
    cd android && ./gradlew assembleRelease
    A release build bundles the JavaScript into the binary, so the app runs without your Metro server.
  2. Find the artifact. Gradle writes it to android/app/build/outputs/apk/release/app-release.apk.
  3. Check the signing config. The stock React Native template signs release builds with the debug keystore until you configure a release keystore in android/app/build.gradle (signingConfigs.release). That installs fine for a tester round — but keep using the same keystore from then on, because switching changes the app's signature and breaks updates (more below).
  4. Upload and share the link. Drag the APK onto BetaDrop's upload page — files up to 500 MB, and a guest upload needs no account. You get an install link and QR code; testers open it in the phone's browser and install directly.

One tester-side note: the first time someone installs an APK from a link, Android asks them to allow installs from their browser (“Install unknown apps”). It is a one-time toggle per app, and our APK install guide walks a non-technical tester through it — worth pasting alongside the build link for a first round. Note that Gradle's bundleRelease task produces an .aab, which is a Play Store publishing format, not an installable file — for testers you want assembleRelease and its APK.

iOS: archive it ad hoc

  1. Open the workspace. Open ios/YourApp.xcworkspace in Xcode — the workspace, not the .xcodeproj, because React Native links its native modules through CocoaPods.
  2. Archive and export for release testing. Product → Archive → Distribute App → Release Testing, choosing a provisioning profile that lists your testers' devices. Xcode 14 and earlier called this option Ad Hoc; from Xcode 15 it is Release Testing, and the Ad Hoc label survives only under Custom. On CI, fastlane's gym with export_method: "ad-hoc" produces the same .ipa.
  3. Cover every tester's UDID. An ad-hoc build installs only on devices whose UDIDs are in the provisioning profile — that is Apple's rule, and no distribution service changes it. Collect each UDID with the UDID checker, add them in the Apple Developer portal, rebuild, and confirm the profile with the provisioning profile decoder.
  4. Upload the .ipa. Upload to BetaDrop and share the link. It generates the itms-services manifest automatically, so testers install over the air from Safari.

Before you send the link, it is worth thirty seconds in the IPA inspector to confirm the embedded profile is the ad-hoc one you expect, has not expired, and carries the device count you think it does — then paste the .mobileprovision into the provisioning profile decoder if you need to see which UDIDs are actually in it. A rebuild that silently picked up an old profile is the most common reason an install button does nothing on a tester's phone. BetaDrop does not re-sign builds and cannot loosen Apple's device rules; it just makes the delivery step instant once the signing is right.

The debug-vs-release trap

A debug build (assembleDebug, or Xcode's Debug configuration) does not contain your JavaScript. It fetches the bundle from the Metro dev server running on your machine — which is exactly why it feels instant while you develop, and exactly why it dies on a tester's phone. The tester opens the app, Metro is not there, and they get the red screen — but the two platforms word it differently: “Unable to load script” on Android, “Could not connect to development server” on iOS.

Testers must get a release build

If a tester reports a red error screen on first launch, you shipped a debug build. Rebuild with ./gradlew assembleRelease on Android or a Release-configuration archive in Xcode — both bundle the JS into the binary so the app runs standalone.

The second classic failure hits on the next build: a tester taps your new link and Android says “App not installed”. Android refuses to install an update whose signing certificate differs from the version already on the device, and React Native projects walk into this constantly — build one came from the template's debug keystore, build two from your new release keystore, or two teammates built from different machines.

Diagnose it in a minute: drop both APKs into the APK signature checker and compare certificate fingerprints. If they differ, standardize on one release keystore (or have testers uninstall the old build once). The full walkthrough is in our guide to the update-incompatible install error.

Automate it from CI

Once the manual path works, stop doing it by hand. The BetaDrop CLI turns every merge into a shareable build. Run this after your workflow has built the APK — note the path is relative to the repository root, not to android/ where step 1 left you:

npm install -g @betadrop/cli
BETADROP_TOKEN=${{ secrets.BETADROP_TOKEN }} \
  betadrop publish android/app/build/outputs/apk/release/app-release.apk --ci

A CI runner has no stored login, so the token has to come from the environment — create one under Settings → Developer → API tokens and store it as a secret. --ci prints only the install URL, so you can pipe it straight into a PR comment or a Slack message. The complete GitHub Actions workflow — token setup, capturing the URL into a step output, posting the link back to the pull request — is on the GitHub Actions integration page.

When this isn't the right page

On Expo's managed workflow? Then there is no android/ folder to cd into and gradlew does not exist — EAS Build produces your binaries in the cloud instead. Everything above still applies once you have an .apk or .ipa in hand, but the build steps are different enough that you should read the Expo internal distribution guide instead. (If you have run npx expo prebuild, you have the native folders and this page is yours.)

And sometimes TestFlight is right. If your iOS beta is public, long-running, and open to strangers, TestFlight's sign-up links and automatic updates beat collecting UDIDs — ad-hoc profiles cap out at 100 devices per device type per year, and that ceiling is real. BetaDrop wins when the testers are a known group and you want the build on their phones in the next five minutes, not after a review. The trade-offs are laid out honestly on our TestFlight comparison page.

Ship the build you just made

You have an app-release.apk or an ad-hoc .ipa. Upload it, send the link, and your testers are running it before your next git pull.

Frequently asked questions

How do I share a React Native APK with testers?

Run cd android && ./gradlew assembleRelease from your project root, grab the APK at android/app/build/outputs/apk/release/app-release.apk, and upload it to BetaDrop. You get an install link and QR code to send testers, and they install straight from the phone's browser — no Play Store, no tester accounts. Files up to 500 MB are supported and a guest upload needs no account.

Why does my React Native app show a red error screen on a tester's phone?

The tester almost certainly has a debug build. Debug builds do not bundle your JavaScript — they load it from the Metro server running on your development machine, which a tester's phone cannot reach, so the app dies on launch: Android's red box reads "Unable to load script", iOS's reads "Could not connect to development server". Build with ./gradlew assembleRelease on Android, or archive the Release configuration in Xcode, so the JS bundle ships inside the binary.

Why does Android say "App not installed" when a tester updates to my new build?

Android refuses to install an update whose signing certificate differs from the version already on the device. In React Native projects this usually means one build was signed with the debug keystore and the next with your release keystore, or two teammates built from different machines. Compare the two APKs' certificate fingerprints with BetaDrop's APK signature checker, then standardize on one release keystore — or have the tester uninstall the old build first.

Can testers install a React Native iOS build without TestFlight?

Yes. Archive with an ad-hoc provisioning profile and upload the .ipa — BetaDrop generates the itms-services manifest, so testers install over the air from Safari. The one rule is Apple's, not ours: an ad-hoc build installs only on devices whose UDIDs are in the provisioning profile, so collect UDIDs before you archive.

Does this work with Expo?

If you have a bare React Native project, or you have run npx expo prebuild, yes — the android and ios folders exist and everything on this page applies. On the managed workflow there is no android folder to cd into; EAS Build produces the .apk or .ipa instead, and our Expo internal distribution guide covers that path.

iMobile Designs
Developed by iMobile Designs
Made with
in India