Skip to Content
Working with the codebaseStore credentials

Store credentials

Shipping to both stores needs three credentials, and the two Apple ones are not interchangeable.

Prerequisites

What each credential does

CredentialWhat it isWhat it covers
App Store Connect API keya .p8 private key, a Key ID and an Issuer IDBuilds, submits, version records, listing text, screenshots, age rating, review submission
Apple ID web sessionyour own Apple Account, with two-factorCreating the app record, and App Privacy. Neither is in Apple’s public API
Google Play service accounta JSON key from Google CloudThe whole Play side: uploads, listing, Data safety, track promotion, review replies

Most of the iOS pipeline runs on the key. Only two steps need the web session, and both are one-offs. On the Android side there is one credential and no web session at all.

Create the App Store Connect key

Pick the access level

Two levels are in play, and they differ:

The key is used forMinimum role
Listing, screenshots, versions, submitting for reviewApp Manager
EAS build credentialsAdmin

eas build registers the bundle identifier and creates distribution certificates and provisioning profiles on the Developer portal, which Apple gates behind Admin. Expo’s Creating an App Store Connect API key  covers where the setting is.

One Admin key for both is simplest, and the rest of these pages assume it. If you would rather not give a submission key Developer-portal rights, generate two: an Admin key for eas.json, an App Manager key for credentials/asc.env. Give them separate filenames. Nothing in this pipeline needs Account Holder.

Generate the key and note three values

Generate a team key in App Store Connect, under Users and Access, in the App Store Connect API integrations.

Download the .p8 immediately. Apple offers that download once, there is no recovery, and a key nobody holds should be revoked rather than left active.

Note the Key ID from the key’s row and the Issuer ID shown above the table. The Issuer ID belongs to the team rather than the key, so regenerating a key later changes only the Key ID and the file.

From your Apple Developer account , under Membership details, note the ten-character Team ID and the entity type, Individual or Organization.

Move the key out of your Downloads folder

Run this in your own terminal:

mkdir -p ~/.appstoreconnect && chmod 700 ~/.appstoreconnect mv ~/Downloads/AuthKey_<KEY_ID>.p8 ~/.appstoreconnect/ chmod 600 ~/.appstoreconnect/AuthKey_<KEY_ID>.p8

The key stays outside the repo. A project directory gets zipped, shared and read by agents, and a private key that was never in it cannot be committed by accident.

macOS denies command-line tools access to ~/Downloads, ~/Desktop and ~/Documents. A terminal listing one of those folders gets Operation not permitted, which looks like an empty folder and is not. Move the file yourself rather than asking an agent to go looking for it, and never revoke a key because a terminal could not find it.

Write credentials/asc.env

credentials/asc.env
export EXPO_ASC_API_KEY_PATH="$HOME/.appstoreconnect/AuthKey_<KEY_ID>.p8" export EXPO_ASC_KEY_ID=<KEY_ID> export EXPO_ASC_ISSUER_ID=<ISSUER_ID> export EXPO_APPLE_TEAM_ID=<TEAM_ID> export EXPO_APPLE_TEAM_TYPE=INDIVIDUAL # or COMPANY_OR_ORGANIZATION # The same key under the names asc reads. export ASC_PRIVATE_KEY_PATH="$EXPO_ASC_API_KEY_PATH" export ASC_KEY_ID="$EXPO_ASC_KEY_ID" export ASC_ISSUER_ID="$EXPO_ASC_ISSUER_ID" # Saves passing --app to every asc command. export ASC_APP_ID=<numeric Apple ID>

The file holds the path, never the key itself. credentials/ is in .gitignore and .easignore, so nothing under it is committed or uploaded with a build.

Two name families appear because eas-cli and asc were written independently. One file sets both, so source credentials/asc.env is all either needs. The submit scripts read the file themselves, so they work in a shell where you forgot to source it.

ASC_APP_ID is the app record’s numeric Apple ID, which does not exist yet on a first release. Add it once the record is created.

Point eas.json at the same key

Under submit.production.ios:

FieldValue
ascApiKeyPaththe same absolute path as EXPO_ASC_API_KEY_PATH
ascApiKeyIdthe Key ID
ascApiKeyIssuerIdthe Issuer ID
appleTeamIdthe Team ID
ascAppIdthe app record’s numeric Apple ID

eas-cli reads eas.json rather than the environment when submitting, which is why the same values appear twice. It insists on all three key fields together; two of three is an error. None of these five values is a secret. The .p8 is the secret, and it is not among them.

The shipped eas.json points ascApiKeyPath at ./credentials/AuthKey_REPLACE_ME.p8. Replace it with the absolute path under ~/.appstoreconnect/.

Verify

node .claude/skills/submit/scripts/preflight.mjs asc auth doctor

Preflight reports a key blocked by macOS as unreadable, never as missing, and tells you to move it. asc auth doctor checks the keychain, the config files and the key files without touching the network.

Register the key in the keychain, or don’t

asc reads ASC_KEY_ID, ASC_ISSUER_ID and ASC_PRIVATE_KEY_PATH from the environment, so credentials/asc.env is enough on its own. It can also hold the key in the macOS keychain under a named profile:

asc auth login --name "<app>" --key-id "<KEY_ID>" --issuer-id "<ISSUER_ID>" \ --private-key ~/.appstoreconnect/AuthKey_<KEY_ID>.p8 asc auth status asc auth switch --name "<app>"

The two mechanisms coexist: asc prefers a stored profile and fills missing fields from the environment. Once stored, commands keep working even if the .p8 is deleted. Keep the file anyway, because it is the only copy Apple will ever give you.

CI has no keychain. Set ASC_BYPASS_KEYCHAIN=1 there, along with ASC_KEY_ID, ASC_ISSUER_ID and the key itself: ASC_PRIVATE_KEY takes the PEM contents and ASC_PRIVATE_KEY_B64 takes it base64-encoded, so the key can be a CI secret rather than a file.

Log in the Apple ID web session

Creating the app record and publishing App Privacy are not in Apple’s public API, so both go through an Apple ID session instead of the key. An agent cannot type a two-factor code, so log the session in yourself once and it is reused until Apple expires it:

asc web auth login --apple-id you@example.com asc web auth status

Create the Google Play service account

One credential covers the whole Play side. eas submit uploads the bundle with it, and every play-*.mjs script authenticates with it.

Create the account and enable the API

In the Google Cloud project linked to your Play Console, create a service account, then enable the Google Play Android Developer API on that project. Without the API enabled the token exchange succeeds and every call afterwards fails.

In Play Console, link the Cloud project under Setup, then invite the service-account email under Users and permissions and grant it Release to testing tracks, Release to production and Manage store presence for this app.

A missing grant surfaces as a 403 on the first push, not as an error at setup time. Expo’s Creating a Google Service Account  has the full walkthrough.

Save the JSON key

Download a JSON key for the service account and save it to credentials/google-play-service-account.json. That path is already wired into eas.json’s submit.production.android.serviceAccountKeyPath and already gitignored.

Google Cloud offers an OAuth client secret from the same Credentials page, and it looks like the right file. A service-account key has "type": "service_account" and a client_email. Preflight names this one specifically, because the wrong file fails much later with an error that says nothing about being the wrong kind of file.

Unlike Apple’s .p8, this key can be replaced: Cloud Console will mint another for the same service account. Delete the old one when you do.

Verify

node .claude/skills/submit/scripts/preflight.mjs

Preflight prints the client_email it found. Nothing local can prove that address has Play Console access, so it names the identity to look for rather than guessing.

The Android upload keystore needs nothing from you. eas build --non-interactive generates it on EAS the first time and reuses it after that.

What never gets committed

Everything under credentials/ and .asc/. Both are in .gitignore and .easignore, so neither lands in a commit nor travels with a build. The .p8 belongs in ~/.appstoreconnect/, outside the repo entirely.

Next

Last updated on