← Back to all posts

13 Xpense

Signing, Keystore, and Releasing an APK

Xpense Dev Retrospective


Why Signing Exists at All

Every Android app has to be cryptographically signed before it can be installed as a release build. Signing proves that whoever built this exact APK holds a specific private key, and it's how Android verifies that an "update" to an already-installed app genuinely came from the same source as the original.

Think of the keystore as a signet ring used to seal a letter with wax. Anyone can read the letter, that's fine, the seal isn't about secrecy. It's about proof: this exact letter, unaltered, came from whoever holds this exact ring. Lose the ring, and there's no way to seal a "continuation" of that same letter ever again, only a brand new one with a different seal.


What a Keystore File Actually Is

A keystore is a file, xpense-keystore.jks in Xpense's case, holding one or more private keys plus their certificates. Generating one produces a key protected by a store password and a key password, along with a key alias identifying that specific key inside the file, since a single keystore file can technically hold more than one key.


How Gradle Actually Uses It

Covered in the Gradle post, but worth repeating here since it's the center of this entire topic:

app/build.gradle.kts
signingConfigs {
    create("release") {
        val keystorePropertiesFile = rootProject.file("keystore.properties")
        val keystoreProperties = Properties()
        if (keystorePropertiesFile.exists()) {
            keystoreProperties.load(FileInputStream(keystorePropertiesFile))
            storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
            storePassword = keystoreProperties["storePassword"] as String
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
        }
    }
}

keystore.properties itself looks roughly like this, and never leaves the local machine:

keystore.properties (shape only, not real values)
storeFile=xpense-keystore.jks
storePassword=REDACTED
keyAlias=REDACTED
keyPassword=REDACTED

The single most important line in this entire series: keystore.properties and xpense-keystore.jks both go in .gitignore. Neither the file nor the credentials inside it are ever committed. The build.gradle.kts file that reads them is completely safe to keep in a public repo, because it contains only the logic to load credentials from somewhere that never gets pushed.


Why Losing the Keystore Is Actually Permanent

There's no "forgot password, reset it" flow for a keystore. If xpense-keystore.jks is lost, or the passwords are forgotten, there is no way to produce a new release build that Android will accept as an update to the existing Play Store listing. Google can't restore it, since Google never has the private key to begin with, that's the entire point of the system. The only path forward at that point is publishing an entirely new app listing, with a new package name, and every existing user has to manually reinstall it, losing their update path and reviews.

This is exactly why the keystore file itself is backed up separately, off the development machine, not just excluded from git. Git deliberately never sees it, but that doesn't mean it should only exist in one place.


Debug and Release Are Provably Different Apps

A debug build is auto-signed by a temporary key Android generates on the machine, different every fresh Android Studio install, good enough for local testing, never meant to be distributed. A release build is signed with the real keystore. Since Xpense's debug build type also applies applicationIdSuffix = ".debug", a debug build and the release build aren't just signed differently, they're literally different package names, installable side by side on the same device without either one interfering with the other.


Actually Building a Release APK

CommandWhat it produces
./gradlew assembleReleaseA signed, minified release APK using the real keystore
./gradlew bundleReleaseAn Android App Bundle (.aab), the format the Play Store actually wants for a new upload

The Play Store specifically expects an .aab, not a raw .apk, since Google generates the actual per-device APKs from the bundle at install time. The signing setup is identical either way, it's the same signingConfigs.release block behind both tasks.


Closing

Signing is the one part of this whole series where a small mistake, an unbacked-up file, a forgotten password, isn't recoverable the way a code bug is. Next up, the last post in this series: what actually happens getting a signed build in front of real users on the Play Store.

← Previous 12 - Theming Done Right Next → 14 - Publishing to the Play Store