Submitting an Expo app with EAS: the checklist
The EAS-specific failures are all versions of one thing: the build that went to the store is not the build you tested.
In the order they bite:
- Build profile drift.
productionsets different environment variables, a different app variant, a different API URL or a different update channel from thepreviewprofile you actually used. Everything you verified was verified on a different app. - Config that never reached the binary. You edited
ios/orandroid/, which prebuild regenerates. Or a config plugin overrode the value. Check the resolved config, notapp.json. - Missing environment variables on the build server. Your machine has a
.env. The build server does not. Values become undefined, and the result is a blank screen or every network call failing, on the reviewer's device only. - Credentials confusion. The wrong provisioning profile, an expired certificate, a bundle identifier that does not match the App Store Connect record.
- The wrong archive. Uploading yesterday's build, which is exactly what EAS Submit exists to prevent.
One command answers most of this:
npx expo config --type publicThat is the configuration after plugins run. It is what shipped. app.json is what you asked for.
Before the build
Check the resolved config, not the source.
npx expo config --type public > /tmp/resolved.json
# purpose strings, capabilities, bundle id, version
grep -E 'UsageDescription|bundleIdentifier|usesAppleSignIn|version|buildNumber' /tmp/resolved.jsonEvery value a reviewer will see comes from here. A purpose string added by a plugin appears in this output and not in app.json, which is why auditing the source file misses it. Auditing what your config plugins added.
Diff your build profiles.
# what actually differs between what you tested and what you ship
python3 -c "import json;d=json.load(open('eas.json'))['build'];\
print(sorted(set(json.dumps(d.get('preview',{}),sort_keys=True).split(','))^\
set(json.dumps(d.get('production',{}),sort_keys=True).split(','))))" 2>/dev/null || cat eas.jsonRead it by hand if that is easier. The question is: what is true in production that was not true in the profile you tested?
Confirm the environment variables exist on the server. Anything read at build time must be set as an EAS environment variable or secret. A .env file that is gitignored is, by definition, not on the build machine.
After the build, before submitting
Install the artifact and use it. Not the simulator build. The actual thing, on a device, signed the way it will ship. Three of six launch bugs in this house's own history were only visible on device, and that is a typical ratio.
Do the reviewer's run. Fresh install, no existing account, no data on the server, offline once. That is the state a reviewer starts in and almost nobody tests it, because your device has your account and your data. What reviewers actually test.
Check the version and build number against what App Store Connect expects. A duplicate build number is a rejected upload, which is cheap, and a wrong version string in the listing is a metadata problem, which is not.
The upload-time blockers
These are decided before a human sees anything, and an Expo project meets them through dependencies rather than through code you wrote:
- ITMS-90683 missing purpose string
- ITMS-91053 required-reason API with no declared reason
- ITMS-91054/5/6 invalid privacy manifest
- ITMS-91061 a listed SDK ships no manifest
Fix all four before uploading, because each one costs a round trip and they are found one at a time.
Then the review-time items
- Account deletion, if the app has accounts
- Sign in with Apple, if you offer social login
- Subscription disclosure, if you sell one
- Other-platform references in metadata and in-app strings
- Screenshots showing the app, on the right device
- Review notes with credentials that work today
Play, in the same pass
Different mechanism, same discipline: check the Console's own blocking list for the version rather than reasoning from memory.
Target API level · Data safety form · Closed testing requirement · Foreground service types
Over-the-air updates
expo-updates after approval is supported and normal. Its limit is that an update must not change what the app fundamentally does, and disclosing that you use it costs three lines in the review notes. Guideline 2.3.1, hidden features.
shipcheck runs this pass against the resolved config, Podfile.lock and node_modules from inside Claude Code, cites the Apple or Google clause for each finding, and names the file and line. What it checks. · The first-submission checklist. · After a rejection.
More in this section
About shipcheck
shipcheck is a pre-submission checker from Baker Ventures LLC that runs inside Claude Code. It reads your React Native or Expo project, finds the things that get apps rejected or blocked at upload, cites the exact App Store Review Guideline or Google Play policy clause, and names the file and line to fix.
It exists because React Native and Expo apps fail for reasons that are invisible in the code you wrote: capability arriving through a dependency, purpose strings added by a config plugin, a privacy manifest missing from an SDK you never chose directly, and a generated ios/ directory that discards your edits on the next prebuild. shipcheck checks what the binary and the config actually declare, not what you intended. The rejection references on this site are free, need no account, and link to the primary Apple or Google document for every claim.
Questions and answers
What is the most common EAS submission mistake?
Shipping a build made from a different configuration than the one you tested. Build profiles set environment variables, app variants, API URLs and update channels, and a production profile that differs from the preview profile you tested is the single most common source of a build that fails review after passing locally.
Why do my app.json changes not appear in the build?
Either a config plugin overwrote them during prebuild, or the value is under a key that only applies to one platform, or you edited the generated ios/ or android/ directory, which is regenerated. Check the resolved config with expo config rather than the source file.
How do I see the config EAS actually used?
npx expo config --type public shows the resolved configuration after plugins run. That is the file to check against the reviewer's report, not app.json, because plugins can add, change and override keys.
What breaks when environment variables are missing in EAS Build?
Anything reading them at build time silently becomes undefined, which usually shows up as an app that launches to a blank screen or fails every network call on the reviewer's device. Local builds pass because your machine has a .env the build server does not.
Should I use EAS Submit or upload manually?
Either works. EAS Submit is less error-prone because it removes the manual step of picking the wrong archive, and the failure it does not protect you from is submitting a build whose configuration was wrong before it was built.
Do over-the-air updates need review?
A JavaScript update that fixes a bug does not require a new submission. An update that changes what the app fundamentally does is the situation Guideline 2.3.1 addresses, and shipping a feature that way that would not have been approved is the case it is aimed at.
shipcheck. “Submitting an Expo app with EAS: the checklist.” Baker Ventures LLC, September 7, 2026. https://shipcheck.bakerventuresstudio.com/guides/expo-eas-submission-checklist/