First-time setup
Do these in order — each step depends on the one before it. Budget about ninety minutes the first time, most of it spent in Apple and Google's web consoles rather than the terminal.
Install leas into your app
From your Expo or React Native project directory:
npm install --save-dev leas-cli npx leas init
leas init writes a leas.json. Its shape deliberately mirrors
eas.json — profiles, extends, and per-platform blocks all behave the
same — so an existing Expo project migrates with little more than a rename.
Create a folder for credentials — and never commit it
mkdir -p credentials/ios credentials/android grep -q '^credentials/' .gitignore || echo 'credentials/' >> .gitignore
A distribution certificate committed to a repo is a certificate you have to revoke. Set the ignore rule first, download second.
Apple — create your distribution certificate
This is the identity that says "this build genuinely came from our team." Generate the key locally so the private half never leaves your machine:
openssl req -new -newkey rsa:2048 -nodes \ -keyout credentials/ios/dist.key \ -out credentials/ios/dist.csr \ -subj "/emailAddress=you@yourcompany.com/CN=Your Company/C=US"
Go to developer.apple.com → Certificates, IDs & Profiles → Certificates → +,
choose Apple Distribution, upload dist.csr, and download the
resulting .cer. Then combine the two halves into the .p12 leas uses:
openssl x509 -in ~/Downloads/distribution.cer -inform DER \ -out credentials/ios/dist.pem -outform PEM openssl pkcs12 -export \ -inkey credentials/ios/dist.key \ -in credentials/ios/dist.pem \ -out credentials/ios/dist.p12 \ -passout pass:CHOOSE_A_PASSWORD
A team can hold only a small number of distribution certificates at once — historically two. If the button is greyed out, someone on your team already used the slots. Find out who before you revoke anything: revoking a certificate instantly invalidates every provisioning profile built on it, which breaks everyone else's builds.
Apple — register the app and create a provisioning profile
Still in Certificates, IDs & Profiles:
- Identifiers → + — register your bundle ID (for example
com.yourcompany.app). It must matchios.bundleIdentifierin yourapp.jsonexactly. - Profiles → + → App Store Connect — pick that identifier, pick the
distribution certificate from step 3, name it something you will recognise in a year, and
download the
.mobileprovisionintocredentials/ios/.
A provisioning profile is just the notarised statement "this certificate may ship this app." That is why it dies whenever either half changes.
Apple — create the App Store Connect API key
This is what leas uses to upload. Go to App Store Connect → Users and Access → Integrations → App Store Connect API, choose Team Keys, and generate a key with the App Manager role.
Record three things:
- The Key ID — a short code like
ABCD1234EF. - The Issuer ID — a UUID shown above the key list, shared by all your keys.
- The .p8 file itself.
Apple lets you download the .p8 once and never again. Save it to
credentials/ios/ and put a copy in your password manager in the same minute. If you
lose it, the only path forward is revoking the key and issuing a new one.
Android — create your upload keystore
keytool -genkeypair -v \ -keystore credentials/android/upload.keystore \ -alias upload -keyalg RSA -keysize 2048 -validity 10000 \ -dname "CN=Your Company, O=Your Company, C=US"
You will be prompted for a password. Use the same one for the store and the key unless you have a reason not to — it keeps the configuration simpler.
With Play App Signing enabled (the default for new apps), Google holds the real app signing key and you hold this upload key. That split is a gift: if you ever lose the upload key, you request a reset and carry on. Losing the app signing key in the old world meant you could never update your app again.
Android — create the Play service account and grant it access
Two halves, in two different consoles. Missing the second half is the single most common setup mistake.
In Google Cloud — IAM & Admin → Service Accounts → Create. Then
open it, go to Keys → Add key → Create new key → JSON, and save the download to
credentials/android/play-service-account.json. Note the account's email, which looks
like leas-publisher@your-project.iam.gserviceaccount.com.
In Play Console — Users and permissions → Invite new users. Paste that service account email, grant it access to your app, and give it the release permissions (at minimum: view app information and manage releases for the tracks you intend to publish to).
Creating the service account in Google Cloud gives it an identity but no authority. If leas
reports 403 · The caller does not have permission, you have almost certainly done
the first half and not the second.
Fill in leas.json
Point the config at what you just created. Copy leas.example.json as your
starting point and edit the identifiers:
"submit": {
"production": {
"ios": {
"ascAppId": "1234567890", // from the App Store Connect URL
"ascApiKeyId": "ABCD1234EF",
"ascApiIssuerId": "69a6de70-...",
"ascApiKeyPath": "credentials/ios/AuthKey_ABCD1234EF.p8",
"waitForProcessing": true
},
"android": {
"packageName": "com.yourcompany.app",
"serviceAccountKeyPath": "credentials/android/play-service-account.json",
"track": "internal"
}
}
}
Build locally first
Always prove it works on your own machine before involving the cloud. Local failures give you the full error; cloud failures give you a log you have to go read.
export LEAS_IOS_DIST_CERT_PASSWORD='the password from step 3' export LEAS_ANDROID_KEYSTORE_PASSWORD='the password from step 6' export LEAS_ANDROID_KEY_ALIAS='upload' npx leas build --platform android --profile production npx leas build --platform ios --profile production
Artifacts land in .leas/build/. When both succeed, submit one by hand to confirm
the upload credentials work end to end:
npx leas submit --platform android --profile production
Upload your secrets and turn on the cloud
Same credentials, different delivery. Everything moves as base64 so it survives being an environment variable:
base64 -i credentials/ios/dist.p12 | gh secret set LEAS_IOS_DIST_CERT_BASE64 base64 -i credentials/ios/profile.mobileprovision | gh secret set LEAS_IOS_PROVISIONING_PROFILE_BASE64 base64 -i credentials/ios/AuthKey_ABCD1234EF.p8 | gh secret set LEAS_ASC_API_KEY_BASE64 base64 -i credentials/android/upload.keystore | gh secret set LEAS_ANDROID_KEYSTORE_BASE64 base64 -i credentials/android/play-service-account.json | gh secret set LEAS_PLAY_SERVICE_ACCOUNT_BASE64 gh secret set LEAS_IOS_DIST_CERT_PASSWORD gh secret set LEAS_ANDROID_KEYSTORE_PASSWORD gh secret set LEAS_ANDROID_KEY_ALIAS gh secret set LEAS_ANDROID_KEY_PASSWORD gh secret set LEAS_ASC_KEY_ID gh secret set LEAS_ASC_ISSUER_ID
Then ship the whole thing from one command:
npx leas build --platform all --profile production --auto-submit --cloud