EAS Build and EAS Submit: From Expo Project to Store-Ready Release

Playcode Team
21 min read
#eas build #eas submit #expo #react native #app store submission

EAS Build compiles an Expo project into a signed iOS or Android binary on Expo-hosted machines. EAS Submit uploads that binary to App Store Connect or Google Play. Together they remove the two things that used to block a solo builder: owning a Mac, and understanding certificate and keystore management well enough to not corrupt a release.

This guide covers what you configure, what EAS manages for you, what only you can supply, and what still breaks. It does not promise a review outcome or a review duration, and it does not claim anyone can publish an app under someone else’s developer account.

Illustrative pipeline from a project file through a cloud signing step to two mobile platforms
Illustrative concept of the EAS build and submit pipeline, not a product screenshot. The actual profiles, credentials, and store steps depend on your project and your developer accounts.

QUICK ANSWER

What does eas build do?

The eas build command compiles your Expo project into a signed iOS or Android binary on Expo-hosted machines, so no Mac and no local Xcode or Android toolchain is required. EAS generates and stores the signing certificates, provisioning profiles, and Android keystore for you. The eas submit command then uploads the finished binary to App Store Connect or Google Play.

Four things to settle before the first build

Three of these cost money or time that you cannot recover later in the process, and one of them cannot be changed after your first store record exists. Settle all four before you run a command.

  • Your own Apple and Google developer accounts: Expo states plainly that Apple Developer Program membership is required to build for the App Store and Google Play Developer membership is required to distribute on Google Play (EAS Build setup, checked 2026-08-16). Both charge a developer fee and both run identity, tax, and banking checks that are not instant. Read the current price and requirements on Apple’s and Google’s own enrolment pages, not from a blog post, because those numbers change and a stale figure will wreck your budget.
  • A bundle identifier and package name you will never change: These are reverse-DNS strings such as com.yourcompany.yourapp. They tie the binary to the store record and to the signing credentials. Renaming after the first submission means a new App Store and Play listing, a new record, and no carry-over of reviews or installs. Pick the real company name, not a placeholder.
  • An Expo account and an access token for it: EAS CLI authenticates with your Expo account. A personal access token set as EXPO_TOKEN lets eas build and eas submit run without an interactive login, which is what an agent or CI job needs. Expo documents this for both platforms (submit to the App Store, submit to Google Play, checked 2026-08-16).
  • A real device to install on: There is no simulator and no emulator anywhere in a Playcode project, by design. Preview builds are installed on a physical phone. Plan for at least one Android device or one iPhone that you can register and install to, and register it before you need it, because ad-hoc iOS distribution requires the device to be on the provisioning profile.

Credentials and access

CredentialMinimum accessStorage and rotation
EXPO_TOKEN
Expo personal access token
Access to the single Expo account or organization that owns this project. Do not reuse a token that also owns unrelated production projects.Project environment variable marked as a secret. In Playcode the agent writes it with the secret flag so it lands in the process environment where eas-cli reads it, and it is never printed into chat, into code, or into a committed file. Revoke and reissue in the Expo dashboard whenever someone leaves the project, whenever a build log is shared outside the team, and on a fixed schedule you actually keep.
App Store Connect API key
Apple API key (issuer ID, key ID, and .p8 private key)
The smallest App Store Connect role that can upload builds for this app. Do not issue an Account Holder key for a submission job.Project environment secret, or uploaded to EAS through eas credentials so the private key never sits in the repository. The .p8 file is downloadable exactly once from Apple, so store it before you close the page. Revoke in App Store Connect and issue a new key on team changes or on any suspicion of exposure. A revoked key breaks submission immediately, so schedule the swap outside a release.
Google Play service account key
Google Cloud service account JSON key granted access in Play Console
Release-management permission on this one app only. Do not grant account-wide admin to a submission robot.Project environment secret. The JSON contains a private key; treat it exactly like a password and keep it out of the repository and out of chat. Delete the key in Google Cloud and remove the service account from Play Console when it is no longer needed. Create a new key rather than sharing an old one across projects.

Decide who holds the signing keys before you decide anything else

EAS can generate and store your signing credentials, or it can use credentials you generate yourself. Expo documents both paths; the managed path prompts you on the first build and reuses the same credentials afterwards (automatically managed credentials, checked 2026-08-16). The choice decides who can ship a release, what happens when a laptop dies, and how much of the process an agent can drive for you.

ApproachBest forTradeoff
EAS-managed credentials, three build profilesAlmost everyone shipping a first app. You get development, preview, and production profiles from eas build:configure, and EAS creates and stores the Android keystore and the iOS distribution certificate and provisioning profile.Your release signing material lives on Expo servers. Expo publishes a security page for exactly this concern, and local credentials remain available if the answer is unsatisfying. In exchange, a lost laptop stops being a release-blocking event.
EAS-managed credentials, production profile onlyA small internal app with no external testers, where every install comes from the store track and nobody needs an internal build.You lose the cheap feedback loop. Preview builds are how you catch a broken API base URL or a crash on launch before a store review sees it, and rebuilding that loop later costs more than keeping the profile.
Bring your own keystore and certificatesAn app that already exists in the stores with signing material you must keep, or an organization whose policy forbids a third party holding release keys.You own key custody, backup, and recovery. Losing an Android upload key without a registered Play App Signing reset path is close to unrecoverable, so this option needs a real secrets process, not a folder on one machine.

Recommended:Start with EAS-managed credentials and three profiles. It is the path the CLI generates, it is the path the documentation assumes, and it is the only one where a single person can ship an update from any machine. Move to your own credentials only when a policy or an existing store record forces it, and write down where those keys live before you make the switch.

How to run an EAS build and submit it to the App Store and Google Play

Configure build profiles and identifiers, load credentials as secrets, verify on a real device, produce the signed store binaries, and upload them to the two consoles.

STEP 01

Enrol both developer accounts and confirm they are active

Enrolment is the slowest, least reversible step, and it blocks the production build rather than the preview build. Start it first.

Enrol in the Apple Developer Program and open a Google Play Developer account under the entity that will own the app. If a company owns the app, enrol the company, not a personal account, because moving a published app between accounts later is a transfer process with its own conditions.

Complete the identity checks, two-factor setup, and the tax and banking forms. An account that shows as pending will pass a casual glance and then fail the first credential prompt.

Check the current developer-account cost on Apple’s and Google’s own enrolment pages before you commit. This guide deliberately does not quote a figure, because both vendors change it and Expo’s own setup page simply states that membership is required for each store.

Expected result: You can sign in to App Store Connect and to Google Play Console and reach the app-creation screen in both, with no outstanding enrolment or agreement banner.

Verify it: App Store Connect shows the Apps section rather than an enrolment prompt, and Play Console offers Create app rather than a blocked account notice. If either still shows a pending agreement, stop here.

STEP 02

Fix the bundle identifier, package name, and version source

These three values decide which store record a build belongs to and whether a second upload is accepted. Set them once, in the app config, before any store record exists.

Use reverse DNS for both identifiers and keep them aligned: com.yourcompany.yourapp on iOS and the same string on Android unless you have a reason to differ.

Set cli.appVersionSource in eas.json so EAS owns the build number rather than a value you hand-edit. The eas.json reference documents the cli block alongside version, requireCommit, and the build profiles. A stale, hand-managed build number is the single most common reason a second upload is rejected.

Create the App Store Connect record and the Play Console record with exactly these identifiers now. Discovering a typo after the first successful build costs a full rebuild.

app.json
{
  "expo": {
    "name": "Your App",
    "slug": "your-app",
    "version": "1.0.0",
    "ios": {
      "bundleIdentifier": "com.yourcompany.yourapp"
    },
    "android": {
      "package": "com.yourcompany.yourapp"
    }
  }
}

Expected result: The app config carries both identifiers, and the App Store Connect and Play Console records use the same strings character for character.

Verify it: Read the identifier back from both consoles and diff it against app.json by eye. Do this before the first build, because after the first upload the identifier is effectively permanent.

STEP 03

Generate eas.json and name the build profiles

A build profile is a named group of build settings. The default file gives you three, and the names are yours to choose.

Run eas build:configure once. It writes eas.json next to package.json with development, preview, and production profiles (eas.json reference, checked 2026-08-16).

The development profile sets developmentClient true and distribution internal. The preview profile is a production-like build without developer tools, meant for your own testers. The production profile is the one that goes to a store.

If you omit --profile, EAS CLI uses the profile named production when it exists. That default has shipped more accidental store builds than any other flag, so pass --profile explicitly in every command you write down.

Use extends to share configuration between profiles rather than copying fields. Expo allows chaining up to five levels deep, which is far more than a first app needs.

Set android.buildType to apk on the preview profile so the file installs straight onto a phone. Leave the production profile alone: it produces an Android App Bundle, which is what Google Play requires for new apps.

eas.json
{
  "cli": {
    "version": ">= 12.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "production": {
      "environment": "production"
    },
    "preview": {
      "extends": "production",
      "distribution": "internal",
      "environment": "preview",
      "android": { "buildType": "apk" }
    },
    "development": {
      "extends": "preview",
      "developmentClient": true,
      "environment": "development"
    }
  },
  "submit": {
    "production": {
      "ios": {
        "ascAppId": "your-app-store-connect-app-id"
      }
    }
  }
}

Expected result: eas.json sits beside package.json with three build profiles and one submit profile, and every profile name you plan to type actually exists in the file.

Verify it: Start a build with --profile preview and read the profile name EAS CLI prints back before it uploads the project. A name that does not exist fails immediately instead of quietly falling through to production.

STEP 04

Load the credentials as environment secrets, never into chat

Three secrets drive the whole pipeline. Each one goes into the environment as a secret value, and none of them belongs in a message, a commit, or a screenshot.

Set EXPO_TOKEN in the environment so eas build and eas submit authenticate without a prompt. Expo documents this exact variable for running the commands from any CI service.

Configure the App Store Connect API key through eas credentials --platform ios, then choose the App Store Connect key option and set the project up to use it for EAS Submit. Expo documents this as the non-interactive path for Apple (submit to the App Store, checked 2026-08-16).

Upload the Google Play service account key so EAS can submit on your behalf. Grant it release management on this app only.

Keep two categories of variable separate. Store credentials belong in the machine environment. Values your app code reads, such as the public API base URL, belong in EAS environment variables, which support development, preview, and production environments and a plaintext, sensitive, or secret visibility per value. A build profile opts in with an environment field.

On Playcode the agent does this from inside the project. It sets each value as a project secret, so the value reaches the process environment that eas-cli reads and never appears in the conversation. If you are running EAS by hand, the equivalent is your shell profile or your CI secret store, and the same rule applies: the value is never echoed.

shell
# Values are supplied as secrets by the environment, never typed inline.
# Confirm presence without printing the value:
[ -n "$EXPO_TOKEN" ] && echo "EXPO_TOKEN is set"

eas whoami                       # must print the account, with no password prompt
eas credentials --platform ios   # attach the App Store Connect API key
eas env:list --environment production

Expected result: eas whoami prints your Expo account without prompting, and neither the token nor the Apple key nor the Play JSON has appeared in any log, commit, or message.

Verify it: Run eas whoami in a fresh shell that has only the environment secrets, not your interactive login. Then search your own chat transcript and git history for the variable names and confirm only the names appear, never the values.

STEP 05

Build the preview profile and install it on a real phone

Preview is the cheap loop. It catches the failures that would otherwise burn a store review: a crash on launch, or an app that cannot reach your backend.

Run eas build --platform android --profile preview first. The Android preview is an apk that installs directly, so it is the fastest way to see the app on a device.

For iOS, internal distribution requires the target device to be registered on the provisioning profile. Register the device before the build, not after, or you will build twice.

There is no simulator and no emulator in a Playcode project. Install on a physical device. If you are running EAS by hand on your own machine, Expo does document simulator and emulator paths, but they prove less than a real handset does about network behavior.

The most common preview failure is not a build failure. It is an app that builds cleanly, launches, and cannot reach the API because a request bypassed the shared client and used a local address. A phone is a different machine from your server, so localhost and any internal port are invisible to it.

Expected result: The build page offers an Install button, the app opens on the phone, and a screen that calls your backend renders real data rather than an error state.

Verify it: Sign in on the phone using an account you created on the website. If the same account works on both, the app is talking to the same backend as the web app, which is the only proof that matters here.

STEP 06

Run the production build and let EAS create the signing credentials

This is where signing happens. On the first production build EAS prompts for credentials and then reuses them on every later build.

Run eas build --platform android --profile production and eas build --platform ios --profile production. Expo’s managed credentials page documents what happens next: on Android you can choose to generate a new keystore, which is then stored on EAS servers; on iOS, generating the distribution certificate and provisioning profile requires signing in with an Apple Developer Program membership.

After the iOS credentials exist, collaborators no longer need access to the Apple Developer team to start a build. That is the single biggest practical benefit of the managed path on a small team.

The Android production profile produces an Android App Bundle. A profile with android.buildType set to apk cannot be submitted to Google Play, so keep that setting on preview only.

Know the platform limits before you debug the wrong thing. Expo documents a maximum build duration per plan, a cap of 50 pending builds per platform per account, and a default medium resource class where large requires a paid plan (EAS Build limitations, checked 2026-08-16).

shell
eas build --platform android --profile production
eas build --platform ios --profile production

eas build:list        # confirm both finished
eas credentials       # confirm the keystore and certificate are attached

Expected result: Both builds finish with a downloadable artifact, and eas credentials lists an Android keystore and an iOS distribution certificate attached to the project.

Verify it: eas build:list shows status finished for both platforms, and the Android artifact is an .aab rather than an .apk. If it is an .apk, your production profile inherited a buildType it should not have.

STEP 07

Submit the binaries, then finish the listing and press submit yourself

EAS Submit uploads. It does not publish. The store listing, the privacy answers, and the final submit button are yours.

Run eas submit --platform ios. It walks you through selecting a build and uploads the binary to App Store Connect. Expo’s documentation states that EAS Submit works on macOS, Linux, and Windows, so no Mac is needed to ship iOS builds, and that the build appears in TestFlight after processing, usually in ten to fifteen minutes (submit to the App Store, checked 2026-08-16). That is a processing figure from the vendor, not a review estimate, and App Review is a separate step with no promised duration and no promised outcome.

Run eas submit --platform android. Expo documents that the first submission creates your app’s first release on the internal testing track, and that the app stays in draft status in Play Console until you complete the store listing and setup tasks (submit to Google Play, checked 2026-08-16).

Passing --auto-submit to eas build chains the two commands so a finished build is handed straight to EAS Submit. Use it once the pipeline is boring, not while you are still debugging it.

The app ships under your own developer account, never under a builder’s. Apple’s guideline 4.2.6 states that apps created from a commercialized template or app generation service will be rejected unless they are submitted directly by the provider of the app’s content (App Store Review Guidelines, read 2026-08-16). Guideline 4.2 is the other one to read: an app should include features, content, and UI that elevate it beyond a repackaged website. An app that only wraps a site is the classic rejection.

On Playcode the agent can drive both commands, draft the listing text, and capture screenshots from the running app. It cannot press submit for you, and it should not: the account and the legal responsibility are yours.

Expected result: The iOS build appears in App Store Connect under TestFlight and the Android build appears in Play Console on the internal testing track, both under your own developer accounts.

Verify it: Open both consoles yourself and confirm the build number matches the one eas build:list reported. Then complete the listing, the privacy answers, and the release form, and press submit. Nothing reaches review until you do.

Four checks that catch the expensive failures

Run all four before you spend a review cycle. Three of them cost a build; the fourth costs nothing and catches the mistake that wastes the most time.

TestScenarioExpected result
happy pathBuild the preview profile for Android, install the apk on a real phone, and sign in with an account you created on the website.The app launches, the sign-in succeeds against the same backend the web app uses, and a screen that reads real data renders it.
invalid inputRun eas build with a --profile name that does not exist in eas.json, for example --profile prod instead of production.EAS CLI fails immediately with an unknown-profile error rather than silently falling back. If it starts a build anyway, you typed a profile that really does exist and you are about to ship the wrong configuration.
retryRun eas submit a second time for a build that already reached the store, without changing the version or build number.The store rejects the duplicate with a version-conflict error. Fix it by letting EAS own the build number through cli.appVersionSource rather than by editing a number by hand and guessing.
production smokeInstall the store build from TestFlight or from the Play internal testing track on a device that has never had a development build on it, and complete one real workflow end to end.The workflow completes against your production backend. A device with an old development build installed can mask a broken configuration, which is why the clean device matters.

What breaks, and what it actually means

These are the failures that look like a mystery and are not. Each one has a distinct signature in the build log or the store console.

SymptomLikely causeCheckFix
The build fails during dependency installation, and only in a monorepo.Workspace hoisting. Expo documents that EAS Build supports monorepos managed by package managers with workspaces, but that third-party monorepo tooling may need extra setup and that official guidance is limited beyond Bun, npm, pnpm, and Yarn.Open the build page and read the Install dependencies phase. A module resolved from the workspace root rather than the app directory is the tell.Follow the Expo monorepo guidance for your package manager and confirm the lockfile at the repository root is the one the build uses. Do not paper over it by committing node_modules.
The build is canceled part-way through with no error from your code.It exceeded the maximum build duration for your plan, or you crossed the cap of 50 pending builds per platform per account.The build page shows a cancellation rather than a failed step. Check eas build:list for a queue of pending builds you forgot about.Cancel the stale queue first. If a single build genuinely needs longer, move that profile to the large resource class, which Expo notes requires a paid plan, and cut work out of the build rather than raising limits forever.
An iOS build or submission stops and asks for an Apple login inside a non-interactive environment.EXPO_TOKEN authenticates your Expo account. It says nothing about Apple. The Apple side needs its own non-interactive credential.The prompt names Apple or App Store Connect, not Expo. eas whoami still succeeds, which proves the Expo half is fine.Set up the App Store Connect API key through eas credentials --platform ios and select the option that configures the project to use the key for EAS Submit.
Google Play rejects the upload with a version-code conflict.The same Android version code has already been uploaded to that track.The Play Console error names the conflicting version code, and eas build:list shows a previous build carrying it.Set cli.appVersionSource so EAS increments the build number, and stop hand-editing it. Two people editing the same number by hand is how the conflict appears in the first place.
Google Play refuses the artifact entirely and asks for an app bundle.The profile produced an .apk. Expo documents that Google Play requires new apps to be published as app bundles, and that an apk build type is for installing on a device rather than for submission.Check the artifact extension on the build page. Then check whether your production profile extends a profile that sets android.buildType to apk.Keep android.buildType apk on preview only. Rebuild the production profile so it produces the default .aab.
The app builds and launches, but every network request fails on the phone while the same code works in a browser.A request bypassed the shared API client and used a local or VM-internal address. A phone is a different machine from the server, so it cannot see localhost.Search the app source for a hardcoded host or port. The failing calls will all be the ones that did not go through the client that reads the public API base URL.Route every request through the shared API client and let the public API origin come from the environment. Build a preview again and confirm sign-in from the phone.

Release, watch, and recover

Deploy

Bump the app version, build the production profile for both platforms, and submit. Keep the two platform releases in the same version so a bug report tells you something.

Roll out through the safe track first: TestFlight on iOS and the internal testing track on Play. Promote only after a real person has completed a real workflow on a real device.

Add --auto-submit to the build command once the pipeline is boring. Chaining an unreliable pipeline just makes the failure harder to read.

On Playcode, take a project snapshot before a release build so the whole project, code and files and database together, can be restored to the state that produced the binary.

Monitor

eas build:list and the Expo build dashboard for build status, queue depth, and the message you attached with --message.

App Store Connect and Play Console for the review state, the release track, and crash reports. Those consoles, not EAS, tell you what users are experiencing.

Your own backend logs. A mobile release changes traffic shape, and the first sign of a bad build is usually an error rate on an endpoint the web app rarely calls.

Recover

Keep the previous store build available. On Play you can set releaseStatus to draft to upload without rolling out, which lets you stage a fix before anyone receives it.

A rejected submission is not a lost build. Fix the listing or the app, rebuild, and submit again under the same store record.

Signing credentials survive a lost laptop when EAS manages them. Run eas credentials to confirm what is stored before you need to find out the hard way.

Restore the project itself from a Playcode snapshot when a release build came from a state you no longer have. Code, files, and database come back together, in place.

Secrets, scope, and what ships inside the binary

A store binary is a client. Anyone can unpack it. Treat everything you put in it as public, and keep everything else out of it.

  • EXPO_TOKEN, the App Store Connect API key, and the Play service account JSON are secrets. They go into the project environment marked as secret, never into chat, never into a commit, never into a screenshot of a terminal.
  • Any environment variable prefixed EXPO_PUBLIC_ is compiled into the app and readable by anyone who downloads it. Put the public API origin there and nothing else.
  • Never ship a backend admin key, a database URL, or a third-party secret key inside the app. Authorization belongs on your server, which is the only place a user cannot edit.
  • Give the Play service account release-management permission on one app rather than account-wide admin, and give the App Store Connect key the smallest role that can upload a build.
  • Rotate every credential when someone leaves the project, and revoke rather than reuse. A revoked key fails loudly at submission time, which is why you rotate outside a release window.
  • Keep the app under your own developer accounts. Apple requires the content owner to submit apps built through a template or app generation service, and it is also the only arrangement where you can still ship if you stop using any particular builder.

Questions people ask about EAS Build

Do I need a Mac to build and submit an iOS app?

No. EAS Build compiles on Expo-hosted machines, and Expo’s own submission documentation states that EAS Submit works on macOS, Linux, and Windows, so you do not need a Mac to ship iOS builds (checked 2026-08-16). You still need an Apple Developer Program membership, which is an account, not a machine.

What is the difference between eas build and eas submit?

eas build produces a signed binary: an .ipa for iOS or an .aab for Google Play. eas submit takes an existing binary and uploads it to App Store Connect or Play Console. They are separate commands because a build is often submitted later, or more than once, or never. Passing --auto-submit to eas build chains them.

What does EAS manage for me, and what do I have to supply?

EAS can generate and store the Android keystore and the iOS distribution certificate and provisioning profile, and reuses them on later builds. You supply the Apple and Google developer accounts, the bundle identifier and package name, the store listing, the privacy answers, and the decision to submit.

How many build profiles do I actually need?

Three, and they are the three eas build:configure creates. Development carries developer tools, preview is a production-like internal build for your own testers, and production is the one that goes to a store. Give preview an apk build type on Android so it installs directly on a phone.

Why did my second upload get rejected when the first one worked?

Almost always a version or build-number conflict: the store already has that number. Set cli.appVersionSource in eas.json so EAS owns the build number instead of a value someone edits by hand. Two people incrementing the same field manually is the usual origin of the conflict.

Can Playcode publish the app for me?

No, and no builder honestly can. In a Playcode project the agent can configure eas.json, load your credentials as project secrets, run eas build and eas submit, draft the listing, and capture screenshots. The developer accounts are yours and you press the final submit yourself. Apple’s guideline 4.2.6 requires exactly that.

How long does App Review take?

Nobody can promise you a number, and anyone who does is guessing. Review takes days, it can ask questions, and it can reject. Build in the assumption of at least one bounce. The durable rule is guideline 4.2: an app that only repackages a website gets refused, so make the app do something real on the device.

Can I test on a simulator or an emulator instead of a phone?

Not in a Playcode project. There is no simulator and no emulator in the stack, by design, so preview builds are installed on a physical device. If you run EAS from your own machine, Expo does document simulator and emulator build paths, but a real handset is what proves network behavior.

Where does the app get its API address from?

From an environment variable holding the project’s public origin, read through the shared API client. A phone is a different machine from the server, so localhost and internal ports are invisible to it. Every request that fails only on the phone is usually a request that skipped the shared client.

Build it on Playcode

Describe the app. Ship it from the project it lives in.

A Playcode project holds the web app, the backend, the database, and the mobile app together. The agent configures the build profiles, loads your credentials as project secrets, runs the build, and drafts the listing. You keep your own developer accounts and press submit yourself.

Start Building

No credit card required. Apple and Google developer accounts are yours and are charged by them, not by Playcode.

Have thoughts on this post?

We'd love to hear from you! Chat with us or send us an email.