contentintech
Learn/mobile dev/App Store Publishing
Advanced~20 min read

App Store Publishing

Ship apps to the iOS App Store and Google Play: developer accounts, iOS certificates and provisioning profiles, Android keystores and Play App Signing, .ipa and .aab artifacts, store listings and ASO, review and testing tracks, and automated releases with fastlane and EAS.

Code SigningProvisioningStore ListingsCI/CD

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:

ProfileCertificateDevicesUse
DevelopmentDevelopmentRegistered UDIDsDebug on device
Ad HocDistributionRegistered UDIDsOff-store test builds (max 100)
App StoreDistributionNoneTestFlight & 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.

PlatformIntermediateUpload 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:

ReasonDetail
Crashes / bugsApp crashes on launch or on the reviewer's device/region
PrivacyMissing privacy policy, undisclosed tracking, labels mismatch actual behavior
In-App PurchaseUsing 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 infoMissing 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

AspectApple App StoreGoogle Play
Account fee$99 / year$25 one-time
Review timeUsually < 24h, human reviewHours to a few days, mostly automated
Upload artifact.ipa.aab (required)
Signing modelCertificates + provisioning profilesKeystore + Play App Signing
Test tracksTestFlight (internal/external)Internal / closed / open
Rollout controlPhased release (7 days)Staged rollout (% based)
Commission15% (Small Business) / 30%15% first $1M / 30%

Practice Exercises

  1. 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.
  2. Generate an Android upload keystore with keytool, wire it into signingConfigs in build.gradle.kts reading the password from an env var, and build a signed .aab.
  3. Set up fastlane match for a team, then write a lane that bumps the build number and uploads to TestFlight.
  4. 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.
  5. 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.
  6. 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.

Section navigation