React Native – Building and Releasing to the App Stores

August 21, 20264 min readUpdated 8/24/2026

The last mile, and the part web developers underestimate most. There is no "push to deploy". A release is a binary, signed with certificates you have to obtain, uploaded to a store, and reviewed by a person who may say no.

Identity: the things you cannot change later

  ios: {
    supportsTablet: true,
    bundleIdentifier: 'com.lovemesomecoding.pizza',
  android: {
    package: 'com.lovemesomecoding.pizza',

The bundle identifier and the Android package name are your app's permanent identity. Once published you cannot change either without shipping a different app that existing users will never receive as an update. Pick reverse-domain names you will still own in five years, and pick them before the first upload rather than after.

Versions: two numbers, different jobs

version is what users see — 1.4.0. The build number (ios.buildNumber, android.versionCode) is what the stores use to order uploads, and it must increase on every single submission, including one that only fixes a typo in a screenshot.

Forgetting to bump it is the most common upload rejection there is. EAS can manage it for you with autoIncrement, which is worth turning on precisely because it is such a dull way to fail.

Building

Locally, npx expo prebuild generates the native projects and you build them with Xcode or Gradle. That requires a Mac for iOS, the right Xcode, and a working signing setup.

EAS Build runs it on machines you do not own:

npx eas build --platform ios --profile production
npx eas build --platform android --profile production
npx eas submit --platform ios

Its real value is not the hardware — it is that it manages credentials. Certificates, provisioning profiles and the Android keystore are the genuinely miserable part of shipping mobile, and a lost keystore means you can never update your Android app again. Letting EAS hold them removes a category of disaster.

Profiles in eas.json describe build variants: a development build with the dev client, an internal build for testers, a production build for the stores.

Configuration without leaking secrets

A mobile bundle is downloadable and inspectable. Anything in it is publicEXPO_PUBLIC_ variables, values in extra, strings in your source. There is no such thing as a secret in a client app.

So: publishable keys, API base URLs and feature flags belong there; API secrets, signing keys and anything that authorises an action do not. If a secret does end up in a build, treat it as compromised and roll it — you cannot recall a binary.

The demo app fails the build loudly rather than guessing when its API URL is missing in a release configuration, on the principle that shipping a binary that silently talks to localhost is worse than not shipping.

Over-the-air updates

EAS Update pushes a new JavaScript bundle to installed apps without a store review. It is the closest thing to a web deploy that mobile has, and it is genuinely useful for a bad copy change or a crash fix.

Its limits are strict and worth knowing. It can only change JavaScript and assets. Adding a native module, changing a permission or bumping the SDK requires a new binary — because the native side is already compiled into the app on the device.

That produces the one real trap: an OTA update expecting native code the installed binary does not have will crash on launch. Updates are keyed to a runtime version for exactly this reason, and getting that wrong is how you brick your users' apps remotely. Both stores permit OTA updates for bug fixes and content; using them to add features that dodge review is against the rules.

Review

A human looks at your app, and both stores reject for reasons that are not bugs.

A demo account. If anything is behind a login, supply working credentials. This is the most common avoidable rejection.

Privacy. Both stores require a privacy policy URL and a declaration of what data you collect. Apple's privacy nutrition labels must match what your app actually does, including what your analytics SDK does on your behalf.

Permission strings. Every iOS permission needs a plain-language reason in Info.plist, and "we need camera access" is not one. Say what for.

Payments. The rule that surprises people: digital goods and subscriptions consumed in the app must use the store's in-app purchase system, which takes a commission. Physical goods and real-world services may use Stripe or anything else — which is why the demo app's pizza checkout is fine.

Expect a rejection or two on the first submission. Read the message; it is usually specific.

Before you press submit

Test the release build, not the debug one. It is a different build — minified, bytecode-compiled, no StrictMode double-render — and bugs do hide there.

Run on a real device. A simulator has no camera, different performance characteristics, and cannot tell you about a keyboard or a notch the way a phone can.

Test a cold start with no network, and a fresh install with empty storage. Both are the first thing a new user experiences and the last thing anyone tests.

Check both platforms. Every lesson in this track has an "Android does this differently" note, and shadows, the back button and the keyboard are the usual suspects.

After it ships

Crash reporting from day one — Sentry or Bugsnag, capturing native crashes as well as JavaScript errors. Upload your source maps, or the stack traces are unreadable.

Then watch the store dashboards. A crash-free-sessions rate is the number that tells you whether the release went well, and it moves before the reviews do.

That is the track

Twenty-five lessons, from create-expo-app to the App Store, every example lifted from one working application. If you build the app alongside them, you will have hit most of the things this track warns about — and recognised them, which is the whole point.

Back to the index.