Skip to Content
Working with the codebaseProduction Builds

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 handEvery time, scriptable
eas logineas init, config.js, eas.json
Apple Developer Program enrolment; a Google Play account for AndroidEAS environment variables
Create one App Store Connect API key and note four valuesStore the key; run every build and submit with --non-interactive
Create the App Store Connect app record and note its Apple IDRead the output, fix what failed, run again
Run the first iOS credential build in your own terminalEvery 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 profileEnvironmentUse
developmentdevelopmentDevelopment build on a device
development-simulatordevelopmentiOS simulator development build
previewpreviewInternal distribution
productionproductionStore 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:

IntegrationVariables
Supabase, requiredEXPO_PUBLIC_SUPABASE_URL, EXPO_PUBLIC_SUPABASE_ANON_KEY
RevenueCatEXPO_PUBLIC_REVENUE_CAT_API_KEY_APPLE, EXPO_PUBLIC_REVENUE_CAT_API_KEY_GOOGLE for the platforms you ship
SuperwallEXPO_PUBLIC_SUPERWALL_API_KEY_APPLE, EXPO_PUBLIC_SUPERWALL_API_KEY_GOOGLE for the platforms you ship
OneSignalEXPO_PUBLIC_ONE_SIGNAL_APP_ID
PostHogEXPO_PUBLIC_POSTHOG_API_KEY and EXPO_PUBLIC_POSTHOG_HOST
SentryEXPO_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 production

yarn 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.env

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

FieldValue
ascApiKeyPathThe absolute path to the .p8, matching EXPO_ASC_API_KEY_PATH
ascApiKeyIdThe Key ID
ascApiKeyIssuerIdThe Issuer ID
appleTeamIdThe Team ID
ascAppIdThe 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:doctor

The 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:info

It 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 production

Expected, 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-interactive

The 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
PromptAnswer
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-interactive

When you must rebuild

Anything that changes native configuration needs a fresh build; Fast Refresh cannot apply it:

  • any edit to config.js or app.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 seeFix
eas init prints Cannot automatically write to dynamic config and exits 1Expected. 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 matchconfig.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 readableA prompting command ran through a pipe. Add --non-interactive, or run it in a terminal if it is the first iOS build
MissingCredentialsNonInteractiveErrorNo 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 modecredentials/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.jsonOne of the three is missing or still REPLACE_ME
An Apple Team ID: or Select your Apple Team Type: prompt appearsEXPO_APPLE_TEAM_ID or EXPO_APPLE_TEAM_TYPE is not exported. Source credentials/asc.env
Bundle identifier "…" is not availableSomeone else registered it. Change iosBundleIdentifier in config.js
The iOS build asks for an Apple ID loginThe key variables were not exported, so eas-cli fell back to the Apple ID flow
Last updated on