Overview
Publishing a mobile app means turning a build into a signed, reviewed, listed product on two very different platforms. Apple's App Store and Google's Play Store each have their own accounts, signing model, artifact format, listing metadata, and review pipeline. This guide walks the full path for both, then shows how to automate the whole thing with fastlane and eas.
Developer Accounts & Enrollment
Before you can distribute anything you need paid developer accounts on both stores.
Apple Developer Program
Enroll in the Apple Developer Program ($99/year). Individual or Organization (organizations require a legal entity and a D-U-N-S number). Enrollment unlocks App Store Connect (where you manage app records, TestFlight, and submissions) and the developer portal (certificates, identifiers, and profiles). Roles like Admin, App Manager, and Developer control who can do what.
Google Play Console
Pay a one-time $25 registration fee for a Google Play Console account. As of 2026, new personal developer accounts must complete identity verification and, for personal accounts created after late 2023, a closed-testing requirement (20 testers for 14 days) before promoting to production. Organization accounts require a D-U-N-S number too.
iOS Code Signing
iOS signing is the part everyone trips on. Three pieces work together: a certificate (proves who you are), an App ID (identifies the app and its capabilities), and a provisioning profile (ties a certificate, App ID, entitlements, and — for non-store builds — a list of device UDIDs into one file the OS trusts).
Certificates
A signing certificate contains a public/private key pair. Two main flavors: Development (run on your own devices during debugging) and Distribution (Apple Distribution, used for TestFlight and the App Store). Certificates live in your macOS Keychain; the private key never leaves your machine unless you export a .p12.
App IDs & Entitlements
An App ID maps to your bundle identifier (e.g. com.acme.app) and enables capabilities like Push Notifications, Sign in with Apple, App Groups, or HealthKit. Those capabilities become entitlements baked into the signed binary and declared in your .entitlements plist.
Provisioning Profiles
A provisioning profile bundles: the App ID, one or more certificates, the entitlements, and (for development/ad hoc) the list of allowed device UDIDs. Three types:
| Profile | Certificate | Devices | Use |
|---|---|---|---|
| Development | Development | Registered UDIDs | Debug on device |
| Ad Hoc | Distribution | Registered UDIDs | Off-store test builds (max 100) |
| App Store | Distribution | None | TestFlight & App Store |
Automatic vs Manual Signing
Automatic signing ("Xcode managed") lets Xcode create and rotate certificates/profiles for you — great for solo work, painful for CI where multiple machines fight over certificates. Manual signing gives you explicit control; on teams and CI it is the norm, usually managed by fastlane match which stores encrypted certs and profiles in a private Git repo so every machine shares one identity.
Back up your signing assets
Losing a distribution certificate is annoying (you can revoke and regenerate). Losing your Android upload key or app signing key without Play App Signing can permanently lock you out of updating an app. Store keys and passwords in a secrets manager, never in Git plaintext.
Android Signing
Android signs the APK/AAB with a key stored in a keystore (a .jks or .keystore file). With modern Play App Signing, there are two keys:
Upload key — you sign the bundle you upload with this. App signing key — Google holds this and re-signs the artifact actually delivered to users. If your upload key is compromised you can request a reset; the app signing key never changes, so users keep getting valid updates. This is why AAB + Play App Signing is the recommended path.
Generate an upload keystore:
keytool -genkeypair -v \
-keystore upload-keystore.jks \
-alias upload \
-keyalg RSA -keysize 2048 -validity 10000
Wire it into build.gradle.kts using signingConfigs, reading secrets from a Gradle property or env var (never hardcode):
android {
signingConfigs {
create("release") {
storeFile = file(System.getenv("KEYSTORE_PATH") ?: "upload-keystore.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = "upload"
keyPassword = System.getenv("KEY_PASSWORD")
}
}
buildTypes {
getByName("release") {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
}
}
}
Build Artifacts
On iOS you build an archive (.xcarchive) then export a signed .ipa for upload. On Android, Google Play now requires the Android App Bundle (.aab) for new apps; Google generates optimized per-device APKs from it. The legacy .apk is still used for sideloading, internal QA, and non-Play distribution, but you cannot publish a fresh app to Play as an APK.
| Platform | Intermediate | Upload artifact |
|---|---|---|
| iOS | .xcarchive | .ipa |
| Android | — | .aab (required) |
Store Listings & ASO
A great binary still needs a great listing. Both stores need metadata, media, and compliance forms.
Metadata & Media
Provide a name/title, subtitle/short description, full description, a 1024x1024 app icon, and screenshots. Apple wants screenshots per device class (6.9" and 6.5" iPhone, 13" iPad); Play wants phone, 7" and 10" tablet sets plus a feature graphic (1024x500). App Preview / promo videos are optional but boost conversion.
App Store Optimization
ASO is the store equivalent of SEO. Apple has a dedicated 100-character keywords field; Play indexes your title, short and full descriptions. Focus on the title and first lines, use relevant terms without stuffing, and iterate on icon/screenshots via store A/B tests (Apple Product Page Optimization, Play Store Listing Experiments).
Privacy, Ratings & Compliance
Apple requires Privacy Nutrition Labels declaring what data you collect and how it is used, plus a privacy policy URL. Google requires the Data safety form plus a Data deletion mechanism. Both need an age/content rating (Apple's age band questionnaire; Google uses IARC). Get these wrong and review will reject or your listing will show mismatched labels.
Testing Tracks & Review
Ship to testers before the public. Apple uses TestFlight (up to 100 internal testers instantly; up to 10,000 external testers after a lightweight Beta App Review). Google Play offers internal, closed, and open testing tracks that promote up to production.
Review Process
Apple's human review against the App Store Review Guidelines typically clears in under 24 hours in 2026. Google Play review is largely automated with human spot-checks and can take hours to a few days, especially for new accounts. Common rejection reasons:
| Reason | Detail |
|---|---|
| Crashes / bugs | App crashes on launch or on the reviewer's device/region |
| Privacy | Missing privacy policy, undisclosed tracking, labels mismatch actual behavior |
| In-App Purchase | Using external payment for digital goods instead of StoreKit / Play Billing |
| Guideline 4.3 (spam) | Duplicate / low-value app similar to many others; template clones |
| Incomplete info | Missing demo account, unclear features, broken links |
Guideline 4.3 watch-out
If your app looks like a mass-produced template or a thin reskin, Apple flags it under 4.3 (Spam). Differentiate real functionality, unique content, and design — reskin factories get whole developer accounts terminated.
Staged & Phased Rollout
Google Play staged rollout lets you release to a percentage of users (e.g. 5% then 20% then 100%) and halt if crash rates spike. Apple's phased release rolls an update out over 7 days automatically; you can pause it in App Store Connect. Always watch crash-free rate and reviews during rollout.
CI/CD Automation
Manual clicking does not scale. Two dominant toolchains: fastlane (Ruby, works with native and RN/Flutter) and EAS (Expo Application Services, cloud build/submit).
fastlane
Key actions: match (sync iOS certs/profiles from a shared repo), gym (build the .ipa), deliver (upload iOS binary + metadata), and supply (upload Android .aab + listing to Play).
# Fastfile
platform :ios do
lane :beta do
match(type: "appstore", readonly: true)
increment_build_number(build_number: latest_testflight_build_number + 1)
gym(scheme: "App")
upload_to_testflight
end
end
platform :android do
lane :internal do
gradle(task: "bundleRelease")
supply(track: "internal", aab: "app/build/outputs/bundle/release/app-release.aab")
end
end
EAS Build & Submit
For Expo / React Native, eas build compiles in the cloud and manages credentials for you, and eas submit uploads to both stores.
eas build --platform all --profile production
eas submit --platform ios --latest
eas submit --platform android --latest
GitHub Actions Example
name: iOS Release
on:
push:
tags: ["v*"]
jobs:
release:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Build & upload to TestFlight
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_KEY_P8 }}
run: bundle exec fastlane ios beta
Use App Store Connect API keys
In CI, authenticate with an App Store Connect API key (issuer ID + key ID + .p8) instead of your Apple ID and password. API keys skip two-factor prompts, scope permissions, and are the only reliable way to run non-interactive TestFlight/App Store uploads.
Versioning
Keep the marketing version (e.g. 2.4.0) human-facing and auto-bump the build number (iOS CFBundleVersion) / versionCode (Android) on every CI run. Both stores reject a build if its build number is not strictly higher than the last uploaded one.
Apple vs Google Comparison
| Aspect | Apple App Store | Google Play |
|---|---|---|
| Account fee | $99 / year | $25 one-time |
| Review time | Usually < 24h, human review | Hours to a few days, mostly automated |
| Upload artifact | .ipa | .aab (required) |
| Signing model | Certificates + provisioning profiles | Keystore + Play App Signing |
| Test tracks | TestFlight (internal/external) | Internal / closed / open |
| Rollout control | Phased release (7 days) | Staged rollout (% based) |
| Commission | 15% (Small Business) / 30% | 15% first $1M / 30% |
Practice Exercises
- Create an App ID with Push Notifications enabled, generate a Distribution certificate, and build an App Store provisioning profile — then explain in one sentence what that profile bundles.
- Generate an Android upload keystore with
keytool, wire it intosigningConfigsinbuild.gradle.ktsreading the password from an env var, and build a signed.aab. - Set up
fastlane matchfor a team, then write a lane that bumps the build number and uploads to TestFlight. - Write a GitHub Actions workflow that builds with EAS and submits to both stores on a tag push, using an App Store Connect API key and a Play service account.
- Draft a complete Play Data safety form and Apple Privacy Nutrition Label for an app that collects email and approximate location for analytics — then justify each declaration.
- Configure a Google Play staged rollout starting at 10%, describe the metric you would monitor, and state the threshold at which you would halt the release.