shipcheck

Submitting an Expo app with EAS: the checklist

Updated September 7, 2026 · published by Baker Ventures LLC · sources cited inline

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:

  1. Build profile drift. production sets different environment variables, a different app variant, a different API URL or a different update channel from the preview profile you actually used. Everything you verified was verified on a different app.
  2. Config that never reached the binary. You edited ios/ or android/, which prebuild regenerates. Or a config plugin overrode the value. Check the resolved config, not app.json.
  3. 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.
  4. Credentials confusion. The wrong provisioning profile, an expired certificate, a bundle identifier that does not match the App Store Connect record.
  5. 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 public

That 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.json

Every 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.json

Read 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:

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

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.

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.

Cite this pageshipcheck. “Submitting an Expo app with EAS: the checklist.” Baker Ventures LLC, September 7, 2026. https://shipcheck.bakerventuresstudio.com/guides/expo-eas-submission-checklist/