Signing and Credentials
Signing keys decide who can ship your app: what EAS stores for you, and why a lost Android keystore can end a listing.
eas credentials --platform androidBoth platforms refuse to install or accept an unsigned app. Signing proves that this binary came from the same publisher as the last one. The keys are not part of your code, and losing them is the one release mistake you cannot fix with a commit.
iOS
Two credentials matter. A distribution certificate is one per Apple Developer account and used for every app. A provisioning profile is per app, lists the registered devices for internal builds, and expires after twelve months. Expiry does not break apps already on the store; it only blocks the next build. A push key is separate, at most two per account, and does not expire.
Android
Android signs with a keystore holding a private key. Apps published since 2021 use Play App Signing: your keystore is only the upload key, and Google holds the key end users verify.
Letting EAS hold them
Run eas credentials. EAS can generate both sets, store them, and reuse them for every build. Set credentialsSource: "local" in a build profile to use a credentials.json in the repo instead.
The one unrecoverable case
Lose an upload key and Google Play support can reset it. Lose the signing key of an older app that is not on Play App Signing and that listing can never be updated again.
Example
// BuildInfo.tsx - confirm which signed build is actually installed
import * as Application from "expo-application";
import { Platform, StyleSheet, Text, View } from "react-native";
type RowProps = { label: string; value: string | null };
function Row({ label, value }: RowProps) {
return (
<View style={styles.row}>
<Text style={styles.label}>{label}</Text>
{/* null means the value is unavailable on this platform */}
<Text style={styles.value}>{value ?? "unavailable"}</Text>
</View>
);
}
export default function BuildInfo() {
return (
<View style={styles.container}>
{/* applicationId is the bundle identifier on iOS, the package name on
Android. Credentials are issued per identifier, so check it first. */}
<Row label="Identifier" value={Application.applicationId} />
<Row label="Version" value={Application.nativeApplicationVersion} />
<Row
label={Platform.OS === "ios" ? "Build number" : "Version code"}
value={Application.nativeBuildVersion}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, gap: 8 },
row: { flexDirection: "row", justifyContent: "space-between" },
label: { fontWeight: "600" },
value: { fontVariant: ["tabular-nums"] },
});When to use it
- Produce your first signed iOS build without manually creating a certificate in the Apple developer portal.
- Take an offline backup of the keystore EAS generated for an app already in production.
- Hand signing over to EAS when the one teammate who had the keystore leaves.
- Register a new tester device and reissue the provisioning profile.
More examples
Inspecting and downloading credentials
Run the first command before your first iOS build so you know whether EAS is generating a certificate or reusing one. The keytool line only matters on the bad day: Play's upload key reset form wants the certificate in PEM form, and this is how you get it out of a keystore.
# Interactive: view, generate or replace the credentials for a profile
eas credentials --platform ios
# Download the Android keystore EAS generated, to keep an offline copy
eas credentials --platform android # choose: download credentials
# Export the upload certificate Google Play support asks for in a key reset
keytool -export -rfc -alias <alias> -file upload_certificate.pem \
-keystore keystore.jksKeeping credentials in the repo instead
Pair this with "credentialsSource": "local" in the build profile. Reach for it when a client or an employer insists the signing material never leaves their control. Note that credentials.json takes the passwords literally: it does not expand environment variables, so a value like $KEYSTORE_PASSWORD is used verbatim and the build fails. Gitignore credentials.json along with the key files, and in CI restore the whole file from a single base64-encoded secret before the build runs.
// credentials.json - only for teams that must hold their own keys
{
"android": {
"keystore": {
"keystorePath": "credentials/release.jks",
"keystorePassword": "KEYSTORE_PASSWORD",
"keyAlias": "upload",
"keyPassword": "KEY_PASSWORD"
}
},
"ios": {
"provisioningProfilePath": "credentials/profile.mobileprovision",
"distributionCertificate": {
"path": "credentials/dist.p12",
"password": "CERTIFICATE_PASSWORD"
}
}
}
Discussion