Identifiers and signing
Bundle ids, package names, signing keys, version numbers: the parts of a mobile app that are hard or impossible to change later. Read it before you create an app, add a flavor, make a key, or bump a version.
How it works
The Android build users get is signed by Google's key, not ours. Anything that checks the app's signature (Google Sign-In, App Links, attribution SDKs) needs the fingerprint of the Play app signing key, found in Play Console after the first upload.
What is burn-once
| Identifier | Can you change it? | So |
|---|---|---|
| iOS bundle id | No. A new id is a new App Store app | Decide the final id before the first upload |
| Android package name | No. A new package is a new Play app | Same |
| Store product ids (in-app purchases) | No. Deleted ids can never be reused | Separate dev product ids |
| Android version code | Must always increase | Never reuse, never go back |
| iOS version + build pair | Each pair uploads once | Bump the build on every upload |
| Android upload key | Resettable through Google, slowly | Keep it in the vault, with a backup |
Keep prototype apps in their own namespace, such as com.example.proto.*, and reserve the
production namespace. Promoting a prototype means a new app record, not renaming the old
one.
How to build it
Android keys
- One upload keystore per flavor, in the team vault, never in git.
- Credentials in a git-ignored
android/key.properties:
prodStoreFile=../keystores/prod/upload.jks
prodStorePassword=...
prodKeyAlias=upload
prodKeyPassword=...
- A pre-commit hook blocks
*.jks,key.propertiesand.env*files. - In CI, store the keystore as a base64 secret and decode it at build time.
After the first Play upload, copy the Play app signing SHA-1 and SHA-256 into
Firebase, every OAuth provider, and /.well-known/assetlinks.json. Add them for every
flavor.
iOS identifiers and capabilities
- Bundle id, display name and backend per flavor live in xcconfig files (Debug, Profile, Release times dev, staging, prod).
- Set the bundle id in both the xcconfig and
project.pbxprojfor the Runner and test targets. Build settings in the project override the xcconfig, so setting only one does nothing, silently. - Capabilities (push, Sign in with Apple, associated domains, App Attest) must be enabled on the App ID before you regenerate the provisioning profile.
- URL schemes for sign-in providers use
$(PRODUCT_BUNDLE_IDENTIFIER)or per-flavor variables. A hard-coded scheme works in one flavor and breaks the others.
Version and build numbers
The marketing version (1.4.0) is for people. The build number is for the stores and
must be unique.
- CI sets the build number, never a person. For example, the run number plus an offset for codes already used by manual uploads.
- Better: ask Play for the highest version code across all tracks and add one.
- Send the version to the backend on every request (
X-App-Version) and the build number separately (X-App-Build). Compare versions as semver, never as strings.
Rules
Never bump version numbers by hand on parallel branches. Two branches will pick the same build number, and one upload will be refused, or worse, ship the wrong code under it.
Pick the final production bundle id and package name before the first store upload. Neither can be renamed.
Losing the upload key stops releases until Google resets it. Losing an iOS distribution certificate is recoverable, but keep a team account, not a person's, as its owner.
Mistakes we made
- The first production iOS bundle id had to be abandoned. It was created under the old scheme and Apple cannot rename it, so production needed a new App Store record and profile. Some docs still name the old id. Keep one source of truth for ids.
- Google Sign-In broke only on the Play-installed build. Firebase had the upload key's SHA, not Play's app signing key's.
- "Your Android App Bundle is signed with the wrong key." The bundle was signed with a different key from the one registered for that app.
- Microsoft and Google sign-in broke on two of three flavors. Redirect schemes were hard-coded to one flavor's values.
- Build numbers collided at least five times from manual bumps on parallel branches.
- Release archives failed after moving from CocoaPods to Swift Package Manager. The
crash-symbol upload step pointed at a path that no longer existed. Commit
Package.resolved, and check build phases after any dependency-manager change.
Checklist
- Final production ids decided; prototypes use their own namespace.
- Upload keystore per flavor in the vault, with a backup; never in git.
- Play app signing SHAs added to Firebase, OAuth providers and assetlinks.
- Bundle id set in xcconfig and
project.pbxproj. - Capabilities enabled on the App ID before regenerating profiles.
- CI sets build numbers.
- The app sends version and build to the backend.