Releasing to App Store & Google Play
Versioning, signing, testing tracks, review rules and rollouts — without surprises.
In 30 seconds
Publishing a mobile app means producing a signed build, uploading it to App Store Connect or the Play Console, testing it with real users, passing review and rolling it out. Each store has its own rules for versions, signing keys, privacy information and content. A written, repeatable release process turns a stressful day into a routine.
Why I use it
- I care about release engineering because a great app that cannot ship reliably is worth nothing to the client.
- Store review rejections and signing problems cost days; I would rather prevent them with a checklist than fix them under deadline pressure.
- Staged rollouts and a force-update switch let me release with confidence and stop a bad build before it reaches everyone.
- I document the release process for each app so any engineer on the team can ship, not only the person who set it up.
How it works
01Versioning: version name vs build number
Every build has two numbers. The user-facing version (iOS CFBundleShortVersionString, Android versionName) follows semantic versioning: 2.4.0 for a feature release, 2.4.1 for a fix. The build number (iOS CFBundleVersion, Android versionCode) is for the stores and must always increase: Google Play rejects a versionCode it has seen, and App Store Connect needs a unique build number per version. Keep one source of truth and let CI bump the build number, for example from the pipeline run number. Tag the git commit with the version so you can always rebuild what is in the store. Also remember OTA updates (if you use them) must be tied to a runtime version so a JavaScript update never lands on a binary with different native code.
// android/app/build.gradle
android {
defaultConfig {
versionCode 2041 // integer, must always increase
versionName "2.4.1" // shown to users
targetSdk 36 // Play: API 36 for new apps and updates from Aug 31, 2026
}
}
// iOS (Xcode build settings):
// MARKETING_VERSION = 2.4.1 -> CFBundleShortVersionString
// CURRENT_PROJECT_VERSION = 2041 -> CFBundleVersion
02Signing: iOS certificates and profiles, Android keystores
Signing proves a build comes from you and was not modified. On iOS you need an Apple Developer Program membership, an App ID (bundle identifier), a distribution certificate (private key in your keychain or CI) and a provisioning profile that ties the certificate, App ID and capabilities (push, Sign in with Apple, associated domains) together. Xcode's automatic signing works for small teams; CI usually uses fastlane match or EAS credentials to share them securely. On Android you sign with an upload key from a keystore file; with Play App Signing (required for new apps, which ship as AAB bundles), Google holds the real app signing key and re-signs. If you lose the upload key you can request a reset; without Play App Signing, a lost key meant you could never update the app. Never commit keystores or passwords to git; store them in a secrets manager or CI secrets.
# Create an Android upload key (keep the file and passwords in a secret store)
keytool -genkeypair -v -keystore upload-key.jks -alias upload \
-keyalg RSA -keysize 2048 -validity 10000
# Build a signed Android App Bundle
cd android && ./gradlew bundleRelease
# iOS: archive and upload with fastlane (certificates via match)
bundle exec fastlane ios beta
03Testing tracks: TestFlight and Play testing
Never send a build to production that real people have not installed from the store's own pipeline. On iOS, upload to App Store Connect and use TestFlight: internal testers (members of your team) get builds almost immediately; external testers (up to 10,000, by email or public link) require a short beta review for the first build of a version. Builds expire after 90 days. On Android, the Play Console has internal testing (fast, up to 100 testers), closed testing (lists or groups) and open testing. Note: new personal developer accounts must run a closed test with at least 12 testers opted in for 14 continuous days before they can apply for production access; organization accounts are exempt. Test the release build, not just debug: minification, signing, push and deep links often behave differently.
- Test upgrade from the previous store version, not only fresh installs
- Check push, deep links, payments and sign-in on the release build
- Keep a review/demo account ready with sample data
04Store listings and privacy information
The listing sells the app and also states facts the stores check. You need a name, subtitle/short description, description, keywords (iOS), screenshots for required device sizes, an icon, category, age rating questionnaire, support URL and a privacy policy URL. Both stores require privacy disclosures: Apple's App Privacy 'nutrition labels' and Google Play's Data safety form must match what the app and its SDKs (analytics, crash reporting, payments) really collect. On iOS, apps and third-party SDKs must include privacy manifests declaring data use and 'required reason' APIs. If the app tracks users across other companies' apps or sites, you must use App Tracking Transparency. Localize listings for each market you target. Prepare screenshots from a real build with realistic, non-personal demo data.
05Review rules and common rejections
Apple reviews every version against the App Review Guidelines; Google combines automated checks with policy review. Most rejections are predictable. Crashes or broken flows during review. Missing demo account or a login wall reviewers cannot pass. Account creation without in-app account deletion (Apple requires it). Third-party social login without an equivalent privacy-focused option such as Sign in with Apple. Selling digital goods or subscriptions outside in-app purchase where it is required (physical goods and real-world services, like food orders, use normal payment providers). Permission prompts without clear purpose strings. Placeholder content, broken links, or an app that is just a website wrapper. Privacy labels that do not match the SDKs. Reply calmly in Resolution Center with specifics; many rejections are resolved by an explanation or a short video.
- Provide a working demo account and notes for the reviewer
- Offer in-app account deletion when users can create accounts
- Write specific permission purpose strings (camera, location, photos)
- Keep store builds on current SDKs: Apple and Google raise minimums yearly
06Staged rollouts, hotfixes and OTA updates
Do not release to 100% at once. On iOS, Phased Release spreads automatic updates over seven days (1%, 2%, 5%, 10%, 20%, 50%, 100%) and can be paused; users can still update manually. On Google Play, a staged rollout lets you pick a percentage and increase it, or halt it if Crashlytics shows a spike. Watch crash-free users, key funnels and support messages before each step. For emergencies, have a hotfix branch and expedited review requests (use sparingly), plus your Remote Config kill-switches. OTA updates (EAS Update, or self-hosted equivalents; Microsoft's CodePush service was retired with App Center in 2025) replace the JavaScript bundle and assets without store review. They are excellent for small fixes, but they cannot change native code, must match the binary's runtime version, and must not change the app's primary purpose — store rules still apply. Roll OTA updates out gradually too.
Use it when
- Every public iOS or Android release, from the very first one.
- Internal and enterprise apps distributed through TestFlight or private Play tracks.
- Hotfix situations, where a prepared process and rollout controls matter most.
Skip it when
- Pure web apps or PWAs that do not need a store presence.
- Very early prototypes — share development builds or internal tracks before investing in full listings.
Alternatives
- EAS Build & Submit
Expo's cloud service that builds, signs and submits iOS and Android apps.
- fastlane
Open-source automation for signing, building, screenshots and uploads, run locally or in CI.
- Firebase App Distribution
Distribute pre-release builds to testers outside the stores.
- Enterprise / MDM distribution
Apple Business Manager custom apps or managed Google Play for internal company apps.
Best practices
- Keep a written release checklist per app, versioned with the code.
- Let CI increment build numbers and tag the git commit for every store build.
- Store signing keys and passwords in a secrets manager; enable Play App Signing.
- Test the signed release build through TestFlight or an internal track, including upgrade from the previous version.
- Keep privacy labels, Data safety and privacy manifests in sync with every SDK you add.
- Use phased/staged rollouts and watch crash-free users before increasing.
- Ship a force-update switch and support in-app account deletion from the first release.
Common mistakes
- Reusing or decreasing the build number, so the upload is rejected at the last minute.
- Losing the signing key or keeping it on one laptop with no backup.
- Submitting without a demo account or with a login only reviewers cannot pass.
- Adding an analytics or ads SDK without updating privacy labels and the Data safety form.
- Pushing an OTA update that depends on native code not present in the installed binary.
Deep dive
07A CI pipeline that builds, signs and uploads
Manual releases fail in boring ways: the wrong scheme, an expired certificate on one laptop, a forgotten build number. Move the whole path into CI. For iOS, fastlane match keeps certificates and profiles encrypted in a private storage repository, and CI pulls them read-only; an App Store Connect API key replaces personal Apple ID logins and two-factor prompts. For Android, CI decodes the upload keystore from a secret, builds the AAB and uploads it to the internal track. Build numbers come from the pipeline run number, the commit is tagged, and the job stores build artifacts and symbol files. Humans keep the decisions that need judgment — promoting to production and increasing rollout percentages — while machines do every repeatable step.
# fastlane/Fastfile
platform :ios do
lane :beta do
setup_ci
app_store_connect_api_key(key_id: ENV["ASC_KEY_ID"], issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_P8"])
match(type: "appstore", readonly: true)
increment_build_number(build_number: ENV["CI_RUN_NUMBER"])
build_app(scheme: "MyApp", export_method: "app-store")
upload_to_testflight(skip_waiting_for_build_processing: true)
end
end
platform :android do
lane :internal do
gradle(task: "bundle", build_type: "Release", project_dir: "android")
upload_to_play_store(track: "internal", aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH])
end
end
08Platform requirements move every year
Both stores raise their minimums on a yearly rhythm, and a release can be blocked by a rule that has nothing to do with your feature. Apple: since 28 April 2026 App Store Connect accepts only builds made with Xcode 26 and the iOS 26 SDK or later; privacy manifests and declared reasons for sensitive APIs are checked at upload. Google: new apps and updates must target API 36 from 31 August 2026, and apps targeting Android 15 or higher must support 16 KB memory page sizes, which means native libraries — including those inside React Native dependencies — must be built for it. Plan a fixed upgrade window each year (Xcode, Android Gradle Plugin, React Native, SDKs) instead of discovering the rule during an urgent hotfix, and subscribe to Apple's and Google's developer news.
09What the stores do with your binary
On Android you upload an App Bundle, not an APK. Google Play splits it into APKs per device configuration — CPU architecture, screen density, language — and signs them with the app signing key, so users download only what their device needs. bundletool lets you reproduce those APKs locally to test exactly what a device would install. On iOS, App Store Connect processes the upload, and App Thinning produces device-specific variants; the App Store Connect size report shows the real download and install sizes per device. Size matters for conversion, especially on mobile data: watch native libraries, bundled fonts and images, and unused locales. Both stores also run automated checks on upload — private API use, missing purpose strings, privacy manifest issues, target SDK — and those messages arrive by email, so someone must read them.
# Reproduce what Play would install on the connected device
bundletool build-apks --bundle=app-release.aab --output=app.apks --connected-device
bundletool install-apks --apks=app.apks
# Check which certificate signed an APK
apksigner verify --print-certs app.apk
# Fingerprints of your upload key (needed for API consoles and resets)
keytool -list -v -keystore upload-key.jks -alias upload
10OTA updates: runtime versions, channels and rollback
An OTA update is only safe if it lands on a binary with exactly the native code it expects. EAS Update tracks that with a runtime version. The appVersion policy ties it to the app version, which is simple but breaks if someone changes native code without bumping the version; the fingerprint policy hashes the native project so any native change produces a new runtime automatically, at the cost of more frequent store builds. Builds listen on a channel (production, staging) mapped to a branch of updates, so you can test an update on staging builds before pointing production at it. Roll out gradually with a percentage, watch crash-free users, and keep a rollback path: republish the previous update or roll back to the update embedded in the binary. Sign updates (code signing) so a compromised server cannot push arbitrary code.
# app.json (expo): runtime changes whenever native code changes
# "runtimeVersion": { "policy": "fingerprint" }
# Publish to 10% of production users first
eas update --branch production --message "Fix checkout crash" --rollout-percentage 10
# Increase, finish or revert the rollout
eas update:edit
eas update:revert-update-rollout
# Emergency: send clients back to the code embedded in the binary
eas update:roll-back-to-embedded
Real-world scenarios
Rejected for missing account deletion
Problem
A new version is rejected under App Review Guideline 5.1.1(v): users can create an account in the app but cannot delete it. The release is promised to the client for Monday.
How to solve it
Add a clear path, usually Settings → Account → Delete account, that starts deletion inside the app — linking only to an email address is not enough. Confirm with the user, explain what happens to their data and orders, and if regulations require keeping some records (invoices), say so. The backend must actually delete or anonymise personal data, revoke tokens and remove push tokens, possibly after a short grace period. If the app uses Sign in with Apple, revoke the token through Apple's REST API. Ship the fix with reviewer notes describing where the option is, attach a short screen recording, and reply in Resolution Center with the build number.
A crash spike at 10% rollout
Problem
Version 3.2.0 went to 10% of Android users and 1% on iOS phased release. Within hours Crashlytics shows crash-free users dropping from 99.8% to 97% on a new payment screen.
How to solve it
Halt the Play staged rollout and pause the iOS phased release immediately; users who already updated keep the build, so turn off the new payment screen with its Remote Config flag to protect them. Read the grouped Crashlytics issue, reproduce on the affected device model, fix on a hotfix branch from the release tag and bump to 3.2.1 with a higher build number. Test the fix through TestFlight and the internal track, including upgrade from 3.2.0. Request expedited review on iOS if the impact justifies it. Resume rollout from a low percentage and write a short incident note: what failed, why tests missed it, which check to add.
The Android upload key was lost
Problem
The only developer who had the upload keystore left the company, and nobody knows the password. CI cannot sign new builds, and a security fix must go out this week.
How to solve it
With Play App Signing the app is not lost — Google holds the app signing key. Generate a new upload key with keytool, export its certificate with keytool -export -rfc -keystore new-upload.jks -alias upload -file upload_certificate.pem, and have the account owner request an upload key reset in Play Console under App integrity, attaching that PEM file. After Google confirms (it can take a few days), sign builds with the new key. Update any API consoles that use the upload key fingerprint, for example Google sign-in or Maps restrictions. Then fix the root cause: keystore and passwords in a team secret store, with access for at least two people.
Cheat sheet
- Set iOS build number from CLI
agvtool new-version -all 2042- Build a signed Android App Bundle
cd android && ./gradlew bundleRelease- Export upload certificate for a key reset
keytool -export -rfc -keystore upload-key.jks -alias upload -file upload_certificate.pem- Verify an APK's signing certificate
apksigner verify --print-certs app.apk- Upload to TestFlight with fastlane
bundle exec fastlane pilot upload- Upload AAB to Play internal track
bundle exec fastlane supply --aab app-release.aab --track internal- Build and submit with EAS
eas build --platform all --profile production && eas submit --platform ios- Publish a gradual OTA update
eas update --branch production --message "Fix" --rollout-percentage 10- iOS gradual release
App Store Connect → version → Phased Release for Automatic Updates- Android gradual release
Play Console → Production → Create release → staged rollout %
In my real projects
- I maintain a documented release process for the mobile apps I ship, including a Remote Config force-update and maintenance switch.
- My apps support deep links through Universal Links and Android App Links, with the association files served by the matching websites.
- An investment app I worked on includes in-app account deletion, consent preferences, an offline banner and force update.
Questions & answers
Q1What is the difference between version and build number?
The version, such as 2.4.1, is what users see and follows semantic versioning. The build number identifies one uploaded binary and must keep increasing — on Android versionCode is an integer that can never repeat, and on iOS each build of a version needs a unique CFBundleVersion. You can upload several builds of 2.4.1 to TestFlight while fixing bugs, each with a higher build number, and release only one. I let CI set the build number automatically and tag the commit so every store build can be traced and rebuilt.
Q2What happens if you lose your Android signing key?
It depends on whether the app uses Play App Signing. With it — mandatory for new apps that ship as App Bundles — Google keeps the app signing key and you only hold an upload key. If you lose the upload key, the account owner can request an upload key reset in Play Console and continue publishing. Without Play App Signing, a lost key meant you could never update the app and had to publish a new listing. That is why I always enable it and keep the upload keystore and passwords in a secrets manager with a backup.
Q3What are the most common App Store rejection reasons and how do you avoid them?
Crashes and broken features during review, missing demo credentials, incomplete metadata or placeholder content, missing in-app account deletion, privacy labels that do not match the SDKs, unclear permission purpose strings, and using external payments for digital content that requires in-app purchase. I avoid them with a pre-submission checklist: test the release build through TestFlight, provide a demo account and reviewer notes, check every permission string, keep privacy information current and make sure sign-in options follow the login guideline. When rejected, I answer in Resolution Center with precise facts.
Q4How do you roll out a release safely?
First the build goes through internal testers on TestFlight and a Play internal track, including upgrade from the previous version. Then I use Apple's Phased Release and a Play staged rollout starting at a small percentage. Before each increase I check Crashlytics crash-free users, error logs and business metrics like completed orders. If something is wrong I pause the rollout and disable the affected feature through Remote Config while a fix goes through review. The force-update switch is the last resort if an old version must stop working.
Q5Are OTA updates allowed, and when would you use them?
Yes, within limits. Apple and Google allow updating interpreted code such as a JavaScript bundle, as long as the update does not change the app's primary purpose, add features that bypass review, or break store rules. Technically an OTA update can only change JavaScript and assets, never native code, so it must target the matching runtime version of the installed binary. I use OTA for urgent bug fixes and copy changes, rolled out gradually with a rollback plan. New features and anything touching native modules still go through the stores.
Senior-level questions
S1How do you keep the backend and old app versions compatible?
Assume every version you ever shipped is still in use somewhere. API changes follow expand-and-contract: add new fields and endpoints first, keep the old ones working, ship apps that use the new shape, and remove the old shape only when analytics show the remaining old versions are negligible. Clients send their app version and platform in headers, so the backend can measure usage, apply version-specific behaviour and return a clear 'update required' error. Remote Config holds the minimum supported version for a forced update, which is the last resort, not the plan, because it interrupts users. The trade-off is carrying compatibility code for months; I record each temporary shim with a removal condition so it actually gets deleted.
S2Design a release process for five apps built from one template.
Standardise the pipeline, not the apps. Each app has the same fastlane or EAS configuration generated from the template, with app-specific values — bundle IDs, signing identities, store metadata, environment URLs — in a small config file and secrets in CI. One shared checklist covers versioning, privacy labels, demo accounts, release-build smoke tests and rollout steps; each app adds only its special cases. Template changes, such as an SDK upgrade, go first through one pilot app's internal tracks, then roll to the others. Every release is tagged and has a short changelog. The trade-off: uniformity sometimes delays one app waiting for the shared change, but it means any engineer can ship any app, and a store rule change is fixed once rather than five times.
S3When is an OTA update the wrong tool, even if it is technically possible?
When it changes what the reviewer approved in a meaningful way — a new payment flow, a new feature area, a change of the app's purpose — because store rules allow interpreted-code updates only within limits, and breaking them risks removal of the app. When the fix depends on native code, a new permission or a changed SDK, because the runtime version does not match. When the update is large or the change risky enough that a staged store rollout with crash monitoring is safer. And when your team lacks rollback discipline: an OTA mistake reaches users within minutes. I use OTA for bug fixes, copy and small layout corrections, always with a gradual rollout, signed updates and a tested rollback path, and keep a written rule for what qualifies.
Glossary
- Bundle identifier
- The unique reverse-domain ID of an iOS app, such as com.example.shop; it cannot change after release.
- Provisioning profile
- An Apple file linking a certificate, an App ID and capabilities, allowing a build to install or ship.
- Distribution certificate
- The Apple certificate and private key used to sign builds for TestFlight and the App Store.
- Upload key
- The key you sign Android bundles with before upload; Google verifies it, then re-signs with the app signing key.
- Play App Signing
- Google storing your app signing key and signing the APKs it delivers, so a lost upload key is recoverable.
- AAB
- Android App Bundle: the upload format from which Google Play generates optimised APKs for each device.
- Phased release
- Apple's seven-day gradual rollout of automatic updates, which can be paused for up to 30 days in total.
- Runtime version
- A label describing a binary's native code, ensuring OTA updates only reach compatible builds.
- Privacy manifest
- A PrivacyInfo.xcprivacy file declaring the data an app or SDK collects and why it uses certain APIs.
- Expedited review
- A request to Apple to review a build faster, meant for critical fixes and time-sensitive events.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.