Android app signing: APK vs AAB, keys and sideloading
· 7 min read
Android signing looks simple until a tester writes back "App not installed". Most of the confusion comes from three things that changed over the years: Google Play moved to app bundles, Play now holds the key that signs what users download, and APK signatures come in several schemes. This guide sorts them out from the angle of someone who needs to put a build on a tester's phone outside the Play Store.
APK vs AAB
An APK is the file Android installs. It contains compiled code, resources and native libraries, and it is signed. You can open it on a phone and install it, as long as the user allows installs from that source.
An AAB (Android App Bundle) is a publishing format. It contains everything needed to build APKs for every device configuration, but it is not itself installable. Google Play takes the bundle, generates optimized split APKs per device (screen density, CPU architecture, language) and serves each device only what it needs. New apps on Google Play have had to use AAB since August 2021.
| APK | AAB | |
|---|---|---|
| Installable on a device | Yes | No |
| Required for new Play apps | No | Yes |
| Who produces the final signed APKs | You | Google Play (or bundletool locally) |
| Good for sharing a test build by link | Yes | Only after converting to an APK |
Why an AAB cannot be sideloaded
A phone does not know what to do with an .aab file. The package installer expects one APK, or a base APK plus matching splits installed together in one session. The bundle is the input to that process, not its output. If you send a tester an AAB, the file simply will not open as an app.
The fix is to turn the bundle into an APK yourself with bundletool, Google's open source tool for app bundles.
Build a universal APK with bundletool
A universal APK contains code and resources for all device configurations in one file. It is larger than what Play would serve, but it installs anywhere the app supports, which is exactly what a test link needs:
bundletool build-apks \
--bundle=app-release.aab \
--output=app-release.apks \
--mode=universal \
--ks=release.jks \
--ks-pass=file:keystore.pwd \
--ks-key-alias=upload \
--key-pass=file:key.pwd
unzip app-release.apks universal.apk -d out/
The .apks output is a zip archive; universal.apk inside it is the file to share. The password flags accept pass:yourpassword or file:/path/to/file; the file form keeps passwords out of your shell history and CI logs. If you leave out the signing flags, bundletool signs with the debug key, which brings the update problems described below. Only feature modules marked for fusing are included in a universal APK, so check that any dynamic feature you want to test is part of it.
If the bundle is already uploaded to Play Console, the App bundle explorer also offers a signed universal APK for download, signed with the Play app signing key.
Play App Signing: app signing key vs upload key
With Play App Signing, which comes with publishing an AAB, there are two keys:
- App signing key: held by Google. Every APK that Play delivers to users is signed with it. This is the key that defines the app's identity on devices.
- Upload key: held by you. You sign the bundle with it before uploading, and Google uses it to verify the upload came from you. If you lose it, the account owner can ask for an upload key reset in Play Console. If you lose the app signing key in an old setup without Play App Signing, you cannot update the app.
This split matters for testing. An APK you build and sign locally carries your upload key. The same app installed from Google Play carries Google's app signing key. Android treats those as different signers, so a tester who has the Play version cannot install your local build over it, and the other way around. Either the tester uninstalls first, or you share an APK signed with the app signing key (for example the one from App bundle explorer). Many teams avoid the clash by giving test builds a different applicationId, such as a .beta suffix, so both versions can sit side by side.
Debug vs release keys
Android Studio signs debug builds with a debug keystore generated on each developer machine, usually at ~/.android/debug.keystore. Two consequences:
- A debug build from developer A and a debug build from developer B have different signatures. Testers moving between them have to uninstall.
- A debug signed build is never accepted by Google Play, and debug builds are also usually debuggable, which you do not want in a client's hands. See security checks before sharing a build.
For anything that leaves the team, sign with a shared release or upload key stored in CI, not with whatever keystore is on the laptop that built it.
Signature schemes v1, v2, v3 and v4
An APK can carry more than one signature format at once. Each newer scheme was added for a reason:
| Scheme | Since | What it adds |
|---|---|---|
| v1 (JAR signing) | The first Android versions | Signs individual files inside the archive. Slower to verify and does not protect all of the APK. |
| v2 | Android 7.0 | Signs the whole file, faster verification, detects any change to the APK. |
| v3 | Android 9 | Same as v2 plus key rotation: the APK can carry a lineage that proves a new key is allowed to replace an old one. |
| v4 | Android 11 | A separate .idsig file used for incremental installs (streaming installs over adb). It needs a v2 or v3 signature alongside it. |
In practice: if your minSdk is below 24, keep v1 enabled, because Android 6 and older only understand v1. For newer minimums, v2 and v3 are what matter. You can check what a file has with apksigner verify --verbose --print-certs app.apk from the Android SDK build tools, or drop it in our APK analyzer, which shows the schemes, the signer name and whether it is a debug key, in the browser and without uploading the file.
"App not installed": signature conflicts on update
When an update fails on a tester's phone, the screen usually says "App not installed" and, on newer versions, "App not installed as package conflicts with an existing package". Over adb the same problem shows as INSTALL_FAILED_UPDATE_INCOMPATIBLE. Android only installs an update when the new APK is signed by the same key as the installed one (or by a rotated key the old one allows through v3). Typical causes:
- Installed from Play (app signing key), updating with a local build (upload key).
- Installed a debug build, updating with a release build, or debug builds from two different machines.
- A new upload key or keystore was created in CI and nobody noticed.
- A lower
versionCodethan the installed build, which fails withINSTALL_FAILED_VERSION_DOWNGRADEover adb.
Fixes, from quickest to cleanest: uninstall and install again (testers lose local app data), give test builds their own application ID, and keep one signing key for all shared builds. If the install fails before it even starts, the problem is often the unknown sources setting instead; see how to enable unknown sources on Android.
A note on developer verification
Google is adding developer verification for apps installed on certified Android devices, including apps installed outside Play. It went live on September 30, 2026 for users in Brazil, Indonesia, Singapore and Thailand, with a global rollout planned for 2027. Developers register their package names and prove ownership with an APK signed by their key, which is one more reason to keep a stable signing key. Google says adb installs stay as they are, there is an advanced flow for installing apps from unverified developers, and a limited distribution option for students and hobbyists covers up to 20 devices. If your testers are in those countries, check Google's developer verification guide for the current state before a test round.
Sharing the APK with testers
Once you have a properly signed APK, universal or not, sharing it is the easy part. Upload it to AppForShare and testers get an install link and QR code that work from the phone's browser. AppForShare takes .apk files, not bundles, so convert the AAB with the bundletool command above first. AppForShare does not re-sign builds, so the signature testers get is the one you made. The guide to sharing an APK link without the Play Store walks through the tester side, and the CI docs show how to upload from a pipeline with npx --yes appforshare@1 upload app-release.apk.
Check your next build in the free APK analyzer, then create a free account to share it.


