How to Ship a React Native Expo App to the App Store with EAS
The full path from a working Expo project to an approved App Store release: build profiles, credentials, versioning, TestFlight, and the review rejections that are easiest to avoid.
Getting an Expo app running on your own device is the easy part. Getting it through App Store review, with working builds, correct credentials, and a versioning scheme that does not collapse on your third submission, is where most of the actual work lives.
This guide covers the path I follow for shipping React Native apps built with Expo and EAS. It assumes you have a working app and focuses on everything between "it runs in Expo Go" and "it is live."
Store dashboards and review policies change frequently. Where this guide references App Store Connect UI, treat it as a description of the shape of the process rather than exact click paths. The CLI commands and configuration files are far more stable than the web interfaces around them.
Why EAS Instead of Local Builds
You can build React Native apps locally with Xcode. EAS Build is worth using anyway, for reasons that compound as a project matures:
- Reproducibility. Builds run in a clean, versioned environment. "Works on my machine" stops being a category of bug.
- Credential management. EAS can generate and store signing certificates and provisioning profiles, which is the single most error-prone part of iOS releases.
- No Mac requirement for iteration. You can trigger iOS builds from any machine, which matters more than expected when you are debugging a CI issue.
The tradeoff is real: you depend on a hosted build service, builds queue on shared infrastructure on free and lower tiers, and debugging a build failure is slower than locally because the feedback loop is longer. For a solo developer or small team shipping a handful of releases a month, that tradeoff is clearly worth it. For an organisation doing continuous native releases with heavy custom tooling, self-hosted runners deserve a serious look.
Prerequisites
Before the first build you need:
- An Apple Developer Program membership (paid, annual)
- An app record in App Store Connect with a chosen bundle identifier
- The EAS CLI, installed and authenticated
npm install -g eas-cli
eas login
eas build:configurebuild:configure creates eas.json and adds a project ID to your app config. Commit both.
Choose your bundle identifier carefully. It is effectively permanent. Changing it after release means a new app record, a new App Store listing, and the loss of your reviews and download history.
Structuring Build Profiles
eas.json defines build profiles. The default scaffold is minimal; a production setup usually wants three distinct profiles with genuinely different purposes.
{
"cli": { "version": ">= 5.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"development-simulator": {
"extends": "development",
"ios": { "simulator": true }
},
"preview": {
"distribution": "internal",
"channel": "preview"
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}What each one is actually for:
- development builds a dev client for physical devices with the debug tooling attached. This is what you install once and then reload JS into. Use it whenever your app has native modules that Expo Go does not include.
- development-simulator extends that profile specifically for the iOS Simulator; a simulator build cannot be installed on a physical device.
- preview produces a release-configuration build distributed internally. This is the profile that catches bugs that only appear in release mode, which is a genuinely distinct category: minification, missing dev-only polyfills, and different error-handling behaviour.
- production is what goes to the store.
Test on a preview build before every release. Release-mode-only bugs are common and development builds will not reveal them. Console warnings that were harmless in dev can become crashes when a dev-only guard is stripped out.
Versioning Without Pain
Two separate numbers matter, and conflating them causes submission rejections.
- Version (
version, shown as CFBundleShortVersionString) is the human-facing release number, e.g.1.4.0. Users see this. - Build number (
ios.buildNumber) must be unique and strictly increasing within a version. Users never see it.
Setting "autoIncrement": true on the production profile lets EAS manage the build number remotely, which removes an entire class of "this build number has already been used" rejections.
{
"expo": {
"name": "My App",
"slug": "my-app",
"version": "1.4.0",
"ios": {
"bundleIdentifier": "com.example.myapp",
"supportsTablet": false
}
}
}The version itself I bump manually, because it is a product decision rather than a mechanical one. Automating it tends to produce meaningless version churn.
If you use a bare workflow or have ejected, the native Info.plist can become the source of truth instead of your app config, and the two silently disagree. Under CNG (Continuous Native Generation), where ios/ is generated rather than committed, the app config wins. Know which model your project uses before debugging a version mismatch.
Credentials
This is where most first-time submissions stall.
iOS requires a distribution certificate (identifies you as the publisher) and a provisioning profile (ties the certificate to a bundle ID and capabilities). EAS can manage both:
eas credentialsLetting EAS generate and store them is the right default for most projects. It handles renewal and keeps the values out of your repository.
Two things worth knowing:
- Apple limits how many distribution certificates an account can hold. If you hit the cap, you must revoke an old one, and revoking a certificate that a live build depends on for resigning can cause problems. Revoke deliberately, not reflexively.
- Provisioning profiles must be regenerated whenever you add a capability such as push notifications, sign in with Apple, or in-app purchases. A build that succeeds but crashes on a specific feature at runtime is often a stale profile.
Never commit certificates, private keys, or App Store Connect API keys to your repository. Use EAS-managed credentials, or store them as encrypted CI secrets. A leaked distribution certificate lets someone sign software as you.
Building and Submitting
eas build --platform ios --profile productionOnce the build succeeds:
eas submit --platform ios --latesteas submit uploads to App Store Connect. For automation, an App Store Connect API key is preferable to an Apple ID with an app-specific password, because it does not break when two-factor prompts appear and it can be scoped to only what it needs.
After upload, the build goes through Apple's processing step before it becomes available in TestFlight. This usually takes minutes but can take considerably longer, and a build can silently fail processing due to issues that the build itself did not catch, most commonly missing export compliance information or an invalid icon asset.
What Actually Gets Apps Rejected
From shipping apps through this process, the rejections that come up repeatedly are rarely about code quality:
Missing or unreachable privacy policy. Apple requires every app to provide a privacy policy link in App Store Connect and make it easily accessible in the app. It must actually load; a URL returning a 404 or requiring authentication is not acceptable.
Incomplete App Privacy answers. The data collection questionnaire in App Store Connect must match what your app genuinely does, including what your third-party SDKs do. An analytics or ads SDK collects data on your behalf, and you are responsible for declaring it.
Account deletion. If your app supports account creation, it must offer in-app account deletion. Pointing users to an email address is not sufficient. This one catches a lot of developers because the requirement arrived after many apps had already shipped sign-up flows.
Broken or unreachable demo credentials. If any part of your app sits behind a login, review needs working credentials, and a reviewer will test them. If your backend rate-limits or the demo account expires, you get rejected for a reason that has nothing to do with your app.
Purchase restoration. If you sell non-consumable products or subscriptions, a visible restore mechanism is mandatory. This is worth its own treatment, which I cover in the RevenueCat tutorial.
Placeholder content. Lorem ipsum, obviously non-final assets, or features that are visibly incomplete will be rejected.
Fill the review notes field properly. Explaining what your app does, how to reach non-obvious features, and why you request a given permission measurably reduces round trips. A reviewer who cannot find your paywall may reject the build for a missing restore button that is actually there.
TestFlight
Do not submit straight to review without a TestFlight pass.
Internal testing (up to 100 team members) requires no review and is available as soon as the build finishes processing. External testing requires a light review pass but lets you distribute to a much larger group.
The specific value of TestFlight is that it is the first time your app runs as a genuine store-signed production build on hardware you do not control. Bugs that appear here and nowhere else are usually about missing entitlements, differences between simulator and device behaviour, or first-launch state you have not seen since the very beginning of the project.
Always test a fresh install, not an upgrade over your development build. First-run experience is the most under-tested path in most apps: empty states, permission prompts, and onboarding all run exactly once and are easy to break without noticing.
Over-the-Air Updates and Their Limits
EAS Update lets you push JavaScript changes without a store review, which is genuinely valuable for fixing a bad copy string or a logic bug within minutes rather than days.
The boundary matters: OTA updates can only change JavaScript and assets. Anything requiring a native change, a new native module, a permission, a capability, or an SDK upgrade, needs a full store build.
Updates are delivered per channel, which is why the profiles above set channel explicitly. An update published to production reaches production builds only.
OTA updates must not be used to materially change what your app does after review. Shipping a different product than the one reviewed violates store guidelines and risks removal. Use updates for fixes and refinements, not to smuggle in unreviewed functionality.
There is also a subtle failure mode: a JS bundle that assumes a native capability the installed binary does not have will crash on launch for users who have not updated. Keep OTA changes compatible with the native build they target, and consider gating new functionality behind a runtime check.
A Realistic Release Checklist
- Bump
versionif this is a user-facing release - Build with the
previewprofile and test in release mode - Test a fresh install, including onboarding and permission prompts
- Verify purchases and restore, if the app monetizes
- Build with the
productionprofile - Submit and wait for processing
- Test the processed build in TestFlight
- Confirm App Privacy answers still match reality
- Update screenshots if the UI changed meaningfully
- Submit for review with useful review notes
Steps 3 and 4 are the ones most often skipped and most often responsible for a bad release reaching users.
Summary
EAS removes most of the mechanical difficulty from React Native releases, but it does not remove the process around them. The parts that reliably cause pain are credentials, the version versus build number distinction, and review requirements that have nothing to do with your code, particularly privacy disclosure, account deletion, and purchase restoration.
The single highest-value habit is testing a release-configuration build on real hardware, from a fresh install, before every submission. Almost every problem I have shipped would have been caught by that one step.
Used In
A circular puzzle game that rethinks falling-block gameplay across a radial 2D board and a fully rotatable 3D globe, with handcrafted levels, progression, online services, and a cosmetic economy.
An offline mobile app that turns "what should I cook tonight" into a spin of the globe: land on a country, get an authentic recipe, and make dinner a small adventure.
A focused mobile workout tracker for daily rep-based challenges, animated exercise progress, streaks, and simple bodyweight consistency.
An offline-first habit tracker where daily rituals grow into a calm 3D grove. Built to make consistency visible, motivating, and less stressful.