Production builds
Build NativeExpress for store distribution with an App Store Connect API key, so every build and submission after the first one runs without prompts.
Prerequisites
Complete app setup and confirm the Expo project link with
eas project:info. Install the EAS CLI and run
eas login with the account that owns the project. Store distribution also needs an
active Apple Developer Program membership or
a Google Play Developer account.
What is done once, and what repeats
eas-cli talks to Expo and to Apple. Expo needs only the eas login session. Apple
needs either an Apple ID with two-factor codes, which no script or agent can type, or
an App Store Connect API key, which eas-cli accepts for everything a build needs.
| Once, by hand | Every time, scriptable |
|---|---|
eas login | eas init, config.js, eas.json |
| Apple Developer Program enrolment; a Google Play account for Android | EAS environment variables |
| Create one App Store Connect API key and note four values | Store the key; run every build and submit with --non-interactive |
| Create the App Store Connect app record and note its Apple ID | Read the output, fix what failed, run again |
| Run the first iOS credential build in your own terminal | Every build after it |
The first iOS build is interactive because eas-cli asks two yes/no questions before it
generates a distribution certificate and provisioning profile, and --non-interactive
exits with MissingCredentialsNonInteractiveError instead of answering them. With the
key in place that run has no Apple login, password or 2FA.
Configure build environments
The included eas.json assigns each build profile to an EAS environment:
| Build profile | Environment | Use |
|---|---|---|
development | development | Development build on a device |
development-simulator | development | iOS simulator development build |
preview | preview | Internal distribution |
production | production | Store distribution |
Create the app’s variables in each environment you use. .env.local is excluded
from the build upload, so configuring it locally does not configure a cloud build. A
cloud build with no environment variables produces an app that throws
Missing Supabase anon key! at launch.
Use EAS environment variables in the Expo dashboard or the CLI. For example, replace the URL below with your hosted Supabase project URL:
eas env:set --environment production --name EXPO_PUBLIC_SUPABASE_URL --value "https://your-project-ref.supabase.co" --visibility plaintext--environment is repeatable, so one command can seed all three. Keep the visibility
plaintext or sensitive: an EXPO_PUBLIC_* value is inlined into the bundle
anyway, and a secret variable cannot be read back with eas env:pull.
Add EXPO_PUBLIC_SUPABASE_ANON_KEY with your public anon or publishable key, then add
the variables for the integrations you enable:
| Integration | Variables |
|---|---|
| Supabase, required | EXPO_PUBLIC_SUPABASE_URL, EXPO_PUBLIC_SUPABASE_ANON_KEY |
| RevenueCat | EXPO_PUBLIC_REVENUE_CAT_API_KEY_APPLE, EXPO_PUBLIC_REVENUE_CAT_API_KEY_GOOGLE for the platforms you ship |
| Superwall | EXPO_PUBLIC_SUPERWALL_API_KEY_APPLE, EXPO_PUBLIC_SUPERWALL_API_KEY_GOOGLE for the platforms you ship |
| OneSignal | EXPO_PUBLIC_ONE_SIGNAL_APP_ID |
| PostHog | EXPO_PUBLIC_POSTHOG_API_KEY and EXPO_PUBLIC_POSTHOG_HOST |
| Sentry | EXPO_PUBLIC_SENTRY_DSN; also EXPO_PUBLIC_SENTRY_URL, EXPO_PUBLIC_SENTRY_PROJECT and EXPO_PUBLIC_SENTRY_ORGANIZATION for source-map uploads |
Use .env.example as the variable checklist. Repeat the configuration for
development and preview if you build with those profiles. Set values for the
backend you want each build to use.
For OneSignal, check the APNs mode against the signing profile for your iOS build, including internal distribution builds.
EXPO_PUBLIC_* values are included in the app bundle. Use public client keys here.
Keep OPENROUTER_API_KEY and REVENUECAT_SECRET_KEY in
Supabase function secrets.
If you use Sentry, add SENTRY_AUTH_TOKEN as a secret in the build environment.
It is used during the build to upload source maps. Follow Monitoring
for the plugin configuration and verification.
Review the selected environment before building:
eas env:list --environment productionyarn skills:doctor also reports which expected variables each environment is missing.
Give eas-cli the App Store Connect key
Generating the key, choosing its access level and storing it are covered on
Store credentials. The key stays at
~/.appstoreconnect/AuthKey_<KEY_ID>.p8, outside the repo. This section is what eas-cli
needs from it.
Source the environment file
source credentials/asc.envDo this in every shell before eas build or eas submit. When EXPO_ASC_API_KEY_PATH,
EXPO_ASC_KEY_ID and EXPO_ASC_ISSUER_ID are set, eas-cli authenticates to Apple with the
key instead of asking for an Apple ID. EXPO_APPLE_TEAM_ID and EXPO_APPLE_TEAM_TYPE
answer the Apple Team ID: and Select your Apple Team Type: prompts.
Fill the submit profile
eas-cli reads eas.json rather than the environment when submitting, so the same values
appear there too. Under submit.production.ios:
| Field | Value |
|---|---|
ascApiKeyPath | The absolute path to the .p8, matching EXPO_ASC_API_KEY_PATH |
ascApiKeyId | The Key ID |
ascApiKeyIssuerId | The Issuer ID |
appleTeamId | The Team ID |
ascAppId | The app record’s Apple ID, from the next step |
eas submit needs all three key fields together; two of three is an error. The shipped
eas.json points ascApiKeyPath at ./credentials/AuthKey_REPLACE_ME.p8, so replace the
whole value rather than only the placeholder.
Create the app record
In App Store Connect, create the app under My Apps: platform iOS, the app name, the
bundle identifier from config.js and any unique SKU. The bundle identifier appears in
the dropdown only once it is registered on the Developer portal, which the first build
does; you can also register it yourself under Identifiers.
The numeric Apple ID is shown on the app’s page under App Information. Put it in
ascAppId. eas submit --non-interactive refuses to run without it, and the
interactive path that would create the record needs an Apple ID session, not the key.
Verify
yarn skills:doctorThe EAS project block reports the linked project, eas.json placeholders still set to
REPLACE_ME, and whether the key file eas.json names exists.
Android is non-interactive from the start: EAS generates the upload keystore the
first time a build needs one. Submission needs a Google Play Developer account, the
app created in the Play Console, and a service-account JSON at
credentials/google-play-service-account.json, the path eas.json already names.
Store credentials covers the permissions
that account needs. The app record has to exist before any Play call works; without it
every request comes back 404 with the package name in it.
Build and submit
Check the project
In config.js, check general.owner, easProjectId, iosBundleIdentifier and
androidPackageName. They must refer to your Expo project and store apps.
eas project:infoIt errors when general.owner or general.slug differ from the project on expo.dev.
Run the first iOS build in your terminal
source credentials/asc.env
eas build -p ios --profile productionExpected, in order, on a fresh Apple account:
✔ Bundle identifier registered com.acme.app
✔ Synced capabilities: Enabled: Push Notifications, Sign In with Apple
? Generate a new Apple Distribution Certificate? › (Y/n)
✔ Created distribution certificate
? Generate a new Apple Provisioning Profile? › (Y/n)
✔ Created provisioning profile
All credentials are ready to build @owner/slug (com.acme.app)Answer Y to both questions. Answering no means uploading your own .p12 and
.mobileprovision files. The capabilities line is not a question: eas-cli reads the
entitlements from the app configuration and enables them on the identifier.
Every prompt eas build and eas submit can show, including the Apple ID route for
anyone who skips the key, is explained in
.claude/skills/setup/references/eas-interactive.md in your project.
Build
source credentials/asc.env
eas build -p ios --profile production --non-interactive
eas build -p android --profile production --non-interactiveThe certificate and profile are now stored on EAS, and the key lets eas-cli renew a profile that has expired or gained a capability. The production profile uses remote versioning and increments the build number. For an existing store app, check its current version sequence in Expo’s app version guide .
Submit
source credentials/asc.env
eas submit -p ios --profile production --latest --non-interactive
eas submit -p android --profile production --latest --non-interactive--latest picks the newest finished build for the platform. iOS lands in TestFlight
processing; Android on the internal track as a draft, per eas.json. This uploads a
binary and nothing else. The listing, the declarations and the release itself are
Submit to the stores.
Verify
Install the submitted build through TestFlight or Google Play’s internal testing track. Confirm that it uses the intended backend, then test sign-in, chat and each optional integration you enabled. Check purchases and Sentry source maps in the distributed build before releasing it.
Development builds on a device
A development build for a physical iPhone (development profile) is an ad-hoc build,
and Apple signs ad-hoc builds only for devices it knows. Device registration is
interactive, so run the first one yourself:
source credentials/asc.env
eas build -p ios --profile development| Prompt | Answer |
|---|---|
Generate a new Apple Distribution Certificate? | Y, if none exists yet |
You don't have any registered devices yet. Would you like to register them now? | Y |
How would you like to register your devices? | Website: open the URL it prints on the iPhone and install the profile |
Press any key if you've already finished device registration. | Any key, once the phone shows the profile installed |
Select devices for the ad hoc build: | Space to tick each phone, then Enter |
Later device builds run with --non-interactive. A new phone means
eas device:create, then --refresh-ad-hoc-provisioning-profile on the next build.
Simulator and Android development builds need nothing from Apple:
eas build -p ios --profile development-simulator --non-interactive
eas build -p android --profile development --non-interactiveWhen you must rebuild
Anything that changes native configuration needs a fresh build; Fast Refresh cannot apply it:
- any edit to
config.jsorapp.config.js - adding or removing a config plugin, including setting the three Sentry variables for
the first time, or
EXPO_PUBLIC_ONE_SIGNAL_APP_ID - upgrading a native dependency
Edits to .env.local need a dev server restart, not a rebuild. Changing an EAS
environment variable reaches users only through a new build; the installed binary keeps
the value it was built with.
Common failures
| What you see | Fix |
|---|---|
eas init prints Cannot automatically write to dynamic config and exits 1 | Expected. The project was created; copy the UUID it printed into general.easProjectId |
eas project:info: Owner of project … does not match or Slug for project … does not match | config.js names a different owner or slug than expo.dev. Fix config.js |
App crashes at launch with Missing Supabase anon key! | The build’s EAS environment had no variables. .env.local is not uploaded |
Input is required, but stdin is not readable | A prompting command ran through a pipe. Add --non-interactive, or run it in a terminal if it is the first iOS build |
MissingCredentialsNonInteractiveError | No distribution certificate on EAS yet. Run the first iOS build in your terminal |
In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode | credentials/asc.env was not sourced in this shell, or one of the three EXPO_ASC_* values is missing |
No devices are registered for this Apple team. | Run eas device:create, then rebuild with --refresh-ad-hoc-provisioning-profile |
Set ascAppId in the submit profile (eas.json) | The App Store Connect app record does not exist yet, or its Apple ID was not copied |
ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId must all be defined in eas.json | One of the three is missing or still REPLACE_ME |
An Apple Team ID: or Select your Apple Team Type: prompt appears | EXPO_APPLE_TEAM_ID or EXPO_APPLE_TEAM_TYPE is not exported. Source credentials/asc.env |
Bundle identifier "…" is not available | Someone else registered it. Change iosBundleIdentifier in config.js |
| The iOS build asks for an Apple ID login | The key variables were not exported, so eas-cli fell back to the Apple ID flow |