App Store & Google Play: The Complete Publication Guide for Developers

App Store & Google Play: The Complete Publication Guide for Developers

Everything developers need to publish apps to Google Play and App Store: signing, tracks, review rules, ASO, CI/CD with fastlane, staged rollouts, and production incidents.

By Omar Flores

Shipping a mobile app is not the same as deploying a web service. When you push to a server, the change is live in seconds. When you submit to Google Play or the App Store, you enter a system with its own rules, its own timelines, its own rejection criteria, and its own audience — the store itself, which decides whether your app reaches users at all.

Most developers learn this the hard way: a binary that works perfectly on every device in the office gets rejected for a missing privacy declaration, or ships with the wrong signing configuration, or passes review but rolls out to users with a crash rate that triggers an automatic halt. The submission process is not just packaging — it is a series of decisions that affect discoverability, stability guarantees, legal compliance, and release velocity.

This guide covers both stores end to end — accounts, signing, release tracks, store listings, review rules, automation, and what to do when something goes wrong after the release is live.


Before You Submit: The Business Layer

Both Google and Apple require a registered developer account before you can submit anything. These are not technical accounts — they are business agreements with annual fees and legal obligations.

Google Play Console — $25 one-time registration fee per developer account. The account can publish unlimited apps. You agree to the Developer Distribution Agreement, which includes content policies, privacy requirements, and API usage restrictions. Registration requires a Google account, payment method, and personal or business identity verification.

Apple Developer Program — $99 per year per account (individual or organization). Organization accounts require a D-U-N-S number from Dun & Bradstreet, which takes 5–14 business days to obtain. This is a real legal entity requirement — Apple verifies that your organization exists. Plan for this lead time before your first submission.

Both accounts can have multiple team members with role-based permissions. Set this up from day one: having a single person as the sole account holder creates a single point of failure for your entire release pipeline.


Google Play: End to End

App Signing

Android apps must be signed with a private key before they can be installed. Google Play uses Play App Signing — Google holds the app signing key, and you upload your app signed with an upload key. This separation is important: if your upload key is ever compromised, you can rotate it without losing the app.

Generate an upload keystore with keytool:

keytool -genkey -v \
  -keystore upload-keystore.jks \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -alias upload

Store the keystore file and passwords somewhere secure — a password manager or a secrets vault, not the repository. If you lose the keystore before enrolling in Play App Signing, you cannot update your app. Ever.

In android/app/build.gradle, configure the signing:

android {
    signingConfigs {
        release {
            storeFile file(System.getenv("KEYSTORE_PATH") ?: "../upload-keystore.jks")
            storePassword System.getenv("KEYSTORE_PASSWORD")
            keyAlias System.getenv("KEY_ALIAS")
            keyPassword System.getenv("KEY_PASSWORD")
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

Never hardcode credentials in build.gradle. Use environment variables, which CI/CD systems can inject securely.

AAB vs APK

Google Play requires Android App Bundle (AAB) format for new apps. APK is only for direct distribution outside the store.

# Flutter
flutter build appbundle --release

# React Native
cd android && ./gradlew bundleRelease

# Native Android (Gradle)
./gradlew bundleRelease

The AAB lets Google generate optimized APKs per device configuration — smaller downloads, faster installs. The output file is typically at build/app/outputs/bundle/release/app-release.aab.

Release Tracks

Google Play has four tracks. Use them in order — do not jump straight to production.

Internal testing — up to 100 testers, invited by email. No review required. Available within minutes of upload. Use this for every build during development. Configure it as your default CI output.

Closed testing (Alpha) — larger tester groups, still invitation-based. Still no review for most apps. Use this for QA sign-off.

Open testing (Beta) — any user can opt in. Apps here go through a basic automated review. Use this for external beta feedback and final pre-production validation.

Production — full review. Available to all users. You control rollout percentage.

A realistic release flow:

Feature complete → Internal build → QA on Internal track
                 → QA passes → Promote to Closed track
                 → External beta feedback → Promote to Open track (optional)
                 → Final sign-off → Submit to Production (10% rollout)
                 → Monitor crash rate for 48h → Roll out to 100%

Versioning

In build.gradle (or pubspec.yaml for Flutter):

android {
    defaultConfig {
        versionCode 42          // integer, must increase with every upload
        versionName "2.1.0"    // human-readable, shown in the store
    }
}

versionCode is what Google uses internally. It must strictly increase — you cannot reuse or decrement it. A common scheme is to compute it from the version components:

// 2.1.0 → 2001000, 2.1.3 → 2001003
def major = 2
def minor = 1
def patch = 0
versionCode = major * 1000000 + minor * 1000 + patch
versionName = "${major}.${minor}.${patch}"

Or for CI environments, use the build number from the CI system:

flutter build appbundle --build-number=$CI_BUILD_NUMBER --build-name=2.1.0

Play Console Setup Checklist

Before your first submission, complete all of these in the Play Console:

  • App content — complete the content rating questionnaire (IARC). Your answers determine the age rating shown in the store.
  • Data safety section — declare every type of data your app collects and shares, how it is used, and whether it is encrypted in transit. This is a legal declaration. Get it right. Missing or inaccurate data safety declarations are a growing cause of rejections.
  • Store listing — app name (max 30 characters), short description (max 80 characters), full description (max 4,000 characters), screenshots (minimum 2, maximum 8 per device type), feature graphic (1024×500px), app icon (512×512px).
  • Privacy policy URL — required for any app that collects data. A real privacy policy, not a placeholder.
  • Target audience — if your app targets children under 13, COPPA and GDPR-K requirements apply and trigger additional restrictions on SDKs and data collection.

Common Google Play Rejection Reasons

Permissions violations. Declaring a permission in AndroidManifest.xml that your app does not actively use will cause a rejection. Audit every <uses-permission> tag and remove anything not needed. Background location access (ACCESS_BACKGROUND_LOCATION) requires explicit justification.

Deceptive store listing. Screenshots that show features not in the current version, or descriptions that claim functionality the app does not have.

Data safety mismatch. If your app uses a third-party SDK (analytics, ads, crash reporting) that collects data, you must declare that data in the data safety section even if your own code does not collect it directly. Check every SDK’s data safety disclosure.

Policy violations in content. Hate speech, sexual content beyond the content rating, dangerous activities. Review the content policy before submission.


App Store (iOS): End to End

Certificates and Provisioning

iOS code signing is the most misunderstood part of Apple’s ecosystem. The components:

Certificate — proves your identity as a developer. Two types: Development (for running on test devices) and Distribution (for App Store submission). Generated in Xcode or on developer.apple.com.

App ID — a unique identifier for your app, matching your bundle ID (e.g., com.yourcompany.yourapp). Registered in the developer portal.

Provisioning profile — combines a certificate + app ID + (for development) a list of authorized device UDIDs. The Distribution profile for App Store does not list specific devices — it allows installation on any device via the App Store.

The simplest management approach for teams is Xcode automatic signing for development and manual signing for release builds in CI:

# In Xcode project settings
CODE_SIGN_STYLE = Automatic          # development
CODE_SIGN_STYLE = Manual             # release/CI
PROVISIONING_PROFILE_SPECIFIER = "AppStore Distribution 2026"
CODE_SIGN_IDENTITY = "Apple Distribution"

For CI, export the certificate as a .p12 file and the provisioning profile as a .mobileprovision file, encode them as base64, and store as CI secrets. Your pipeline decodes and installs them before building.

Building and Archiving

An App Store submission requires an archive built with the Distribution certificate.

# Xcode command line
xcodebuild archive \
  -scheme YourApp \
  -configuration Release \
  -archivePath build/YourApp.xcarchive \
  CODE_SIGN_STYLE=Manual \
  PROVISIONING_PROFILE_SPECIFIER="AppStore Distribution 2026"

# Export IPA from archive
xcodebuild -exportArchive \
  -archivePath build/YourApp.xcarchive \
  -exportPath build/output \
  -exportOptionsPlist ExportOptions.plist

ExportOptions.plist controls the export method:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store-connect</string>
    <key>teamID</key>
    <string>YOUR_TEAM_ID</string>
    <key>uploadBitcode</key>
    <false/>
    <key>signingStyle</key>
    <string>manual</string>
    <key>provisioningProfiles</key>
    <dict>
        <key>com.yourcompany.yourapp</key>
        <string>AppStore Distribution 2026</string>
    </dict>
</dict>
</plist>

For Flutter:

flutter build ipa --release
# IPA at build/ios/ipa/YourApp.ipa

App Store Connect Setup

Bundle ID — must match exactly what is in your Xcode project. Register it in the developer portal before creating the app in App Store Connect.

Build numberCFBundleVersion in Info.plist. Must increase with every build uploaded to App Store Connect, even TestFlight builds. CFBundleShortVersionString is the user-visible version string.

# Set via agvtool
agvtool new-marketing-version 2.1.0
agvtool new-version -all 42

App information — name (max 30 characters on the store, but your bundle name can be longer), subtitle (max 30 characters, shown below the name in search), category (primary and optional secondary), content rights declaration.

App Privacy — declare every data type your app collects, the purpose, and whether it is linked to the user’s identity. Like Google’s data safety section, this covers third-party SDKs. Apple shows this as the “Privacy Nutrition Label” on the app’s store page.

Age Rating — based on a questionnaire about content: violence, sexual content, gambling, drug use, user-generated content. Answer accurately — misrepresenting age-appropriate content is grounds for removal.

Screenshots — required sizes:

  • iPhone 6.9” (1320×2868 or 1290×2796) — used as default for all iPhone sizes
  • iPad Pro 13” (2064×2752 or 2048×2732) — required if your app supports iPad
  • Each screenshot is actually a 1242×2688px image shown in a device frame on the store

Screenshots must show actual app UI — no marketing images that do not represent the app. Apple reviewers check this.

TestFlight

TestFlight is Apple’s beta distribution platform, built into App Store Connect. Every build uploaded to App Store Connect is automatically available in TestFlight before going to review.

Internal testers — up to 100 members of your App Store Connect team. Available within minutes of upload, no review required. Builds expire after 90 days.

External testers — up to 10,000 users invited by email or a public link. Requires a basic TestFlight review (usually within 24 hours, faster than full App Store review). Use this for beta programs.

TestFlight is the right way to distribute pre-release builds to stakeholders. Do not use ad-hoc distribution or enterprise certificates for this — it violates Apple’s guidelines and those certificates are meant for internal device testing only.

App Store Review

Apple’s review process typically takes 24–48 hours for new apps and 24 hours for updates. Expedited review is available for critical bug fixes — use the request form in App Store Connect, explain the user impact, and include the specific build. Apple grants expedited review for genuine production issues, not for missed deadlines.

Common rejection reasons:

Guideline 2.1 — Performance: App Completeness. Crashes during review, placeholder content (“Lorem ipsum”), or demo/test features visible in the production build. Reviewers test your app. Crashes fail immediately.

Guideline 4.0 — Design. Apps that are too simple — a single web view of a website, a PDF reader with no additional features, or apps that replicate the functionality of a built-in iOS app without adding value.

Guideline 5.1.1 — Privacy: Data Collection. Requesting access to contacts, location, camera, microphone, or health data without a clear purpose string in Info.plist. The purpose string must explain specifically why the app needs access — “for better experience” is rejected. Be specific.

Guideline 3.1.1 — In-App Purchase. Selling digital goods or subscriptions through a payment method other than Apple’s IAP system. This includes linking to a website to buy a subscription that is then available in the app.

Guideline 1.4 — Physical Harm. Apps that provide dangerous information without appropriate warnings: medical dosage information, legal advice, financial advice. Add disclaimers.

When you receive a rejection, read the specific guideline cited. Reply in the Resolution Center with the exact change you made and where to find it in the app. Do not argue — address the specific concern and resubmit.


App Store Optimization (ASO)

ASO is how users find your app through search within the store. It directly impacts organic downloads without any advertising spend.

App name. Include your primary keyword in the name. Users search for what the app does, not what it is called. “Planify — Task Planner” ranks for both “Planify” and “task planner.” Google Play gives more weight to the name than Apple does.

Keywords field (iOS only). 100 characters, comma-separated, no spaces after commas. Apple does not show this to users — it is purely for search indexing. Do not repeat words already in your name or subtitle. Use the full 100 characters.

task,planner,todo,calendar,reminder,schedule,productivity,work,goals,habit

Description. Google Play indexes the full description for search. Front-load the most important keywords in the first three lines (visible without tapping “more”). Apple does not index the description for search — focus on conversion, not keywords.

Screenshots. The first two screenshots are shown in search results before the user taps the listing. These drive conversion more than any other asset. Make them functional explanations of the app’s core value, not decorative images.

Ratings. Both stores use average rating and review count as ranking signals. Prompt for ratings at a moment of success — after completing a task, after a positive interaction — not on launch or after an error. Use the platform’s native rating prompt (not a custom one that filters negative reviews to a form instead of the store — this violates both stores’ guidelines).

Release notes. Users read release notes before updating. Write them for users, not for your team’s internal ticket system. “Various bug fixes and performance improvements” is a missed opportunity. Describe what changed, what improved, and what is new.


Automating with Fastlane

Fastlane is the standard tool for automating mobile release pipelines. It handles building, signing, screenshot generation, metadata management, and uploading to both stores from a single Fastfile.

Install:

gem install fastlane
# or
brew install fastlane

Initialize in your project root:

fastlane init

A Fastfile for a Flutter app with both stores:

# fastlane/Fastfile

default_platform(:ios)

platform :ios do
  desc "Run tests"
  lane :test do
    run_tests(scheme: "Runner")
  end

  desc "Build and upload to TestFlight"
  lane :beta do
    build_number = ENV["CI_BUILD_NUMBER"] || "1"

    # increment build number
    increment_build_number(
      build_number: build_number,
      xcodeproj: "ios/Runner.xcodeproj"
    )

    # build
    sh("flutter build ipa --release --build-number=#{build_number}")

    # upload to TestFlight
    upload_to_testflight(
      ipa: "build/ios/ipa/Runner.ipa",
      skip_waiting_for_build_processing: true
    )
  end

  desc "Submit to App Store"
  lane :release do
    upload_to_app_store(
      ipa: "build/ios/ipa/Runner.ipa",
      skip_screenshots: false,
      skip_metadata: false,
      submit_for_review: false,   # set true to auto-submit after upload
      automatic_release: false
    )
  end
end

platform :android do
  desc "Build and upload to Internal track"
  lane :internal do
    build_number = ENV["CI_BUILD_NUMBER"] || "1"

    sh("flutter build appbundle --release --build-number=#{build_number}")

    upload_to_play_store(
      track: "internal",
      aab: "build/app/outputs/bundle/release/app-release.aab",
      json_key: ENV["GOOGLE_PLAY_JSON_KEY_PATH"]
    )
  end

  desc "Promote from internal to production with staged rollout"
  lane :promote_production do
    upload_to_play_store(
      track: "internal",
      track_promote_to: "production",
      rollout: "0.1",             # 10% staged rollout
      json_key: ENV["GOOGLE_PLAY_JSON_KEY_PATH"]
    )
  end
end

For Google Play automation, generate a service account JSON key in the Google Cloud Console with the “Service Account” role in Play Console. Store the JSON as a CI secret.

For App Store automation, use App Store Connect API keys (not your personal Apple ID). Generate a key in App Store Connect → Users and Access → Integrations → App Store Connect API. Store the private key (.p8 file), Key ID, and Issuer ID as CI secrets.

GitHub Actions Pipeline

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.29.0'

      - name: Decode keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/upload-keystore.jks

      - name: Build AAB
        env:
          KEYSTORE_PATH: upload-keystore.jks
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: flutter build appbundle --release --build-number=${{ github.run_number }}

      - name: Upload to Play Store
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
          packageName: com.yourcompany.yourapp
          releaseFiles: build/app/outputs/bundle/release/app-release.aab
          track: internal

  ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.29.0'

      - name: Install certificate and profile
        env:
          CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
          CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
          PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
        run: |
          # create keychain
          security create-keychain -p "" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "" build.keychain

          # install certificate
          echo "$CERTIFICATE_BASE64" | base64 -d > certificate.p12
          security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign

          # install provisioning profile
          echo "$PROVISIONING_PROFILE_BASE64" | base64 -d > profile.mobileprovision
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/

      - name: Build IPA
        run: flutter build ipa --release --build-number=${{ github.run_number }}

      - name: Upload to TestFlight
        env:
          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_PRIVATE_KEY }}
        run: |
          xcrun altool --upload-app \
            --type ios \
            --file build/ios/ipa/Runner.ipa \
            --apiKey "$APP_STORE_CONNECT_API_KEY_ID" \
            --apiIssuer "$APP_STORE_CONNECT_ISSUER_ID"

Staged Rollouts and Monitoring

Google Play Staged Rollout

Never release to 100% immediately. Use staged rollouts — release to a percentage of users, monitor crash rate and ANR rate, then expand.

In Play Console, set the rollout percentage when promoting to production. You can increase or halt the rollout at any time.

Halt criteria: if the crash-free sessions rate drops below 99.5% or the ANR rate increases measurably, halt the rollout before expanding. A halted rollout does not remove the app from existing users — it stops the rollout to new users while you investigate.

Day 0:  Release at 10%
Day 2:  Crash rate stable → expand to 30%
Day 4:  Stable → expand to 50%
Day 6:  Stable → expand to 100%

With fastlane:

lane :expand_rollout do |options|
  upload_to_play_store(
    track: "production",
    rollout: options[:percentage].to_s,
    skip_upload_aab: true,
    json_key: ENV["GOOGLE_PLAY_JSON_KEY_PATH"]
  )
end

# fastlane expand_rollout percentage:0.5

App Store Phased Release

Apple’s equivalent is Phased Release — automatically rolls out to 1%, 2%, 5%, 10%, 20%, 50%, then 100% over 7 days. You can pause a phased release for up to 30 days.

Enable it in App Store Connect when submitting the version: Phased Release → On.

What to Monitor Post-Release

Set up monitoring before your first production release — not after something breaks.

Crash reporting. Firebase Crashlytics is the standard for both platforms. It surfaces crash-free rate, crash count by version, and symbolicated stack traces. Set alert thresholds at 0.5% crash rate.

ANR rate (Android). Application Not Responding — the Android-specific hang that occurs when the main thread is blocked for more than 5 seconds. Play Console shows this under Android Vitals. High ANR rates can cause Play to reduce your app’s visibility in search.

Core Web Vitals equivalents for mobile — Android Vitals:

  • Crash rate < 1.09% (Play’s current threshold for “bad behavior” flagging)
  • ANR rate < 0.47%
  • Excessive wakeups, stuck partial wake locks, excessive background WiFi scans

Monitor these from day one. Play uses Android Vitals data to penalize apps in search ranking.


Emergency: Pulling a Release

Sometimes a release ships with a critical bug. The options differ between platforms.

Google Play. You cannot remove a version that is already installed on user devices. You can:

  1. Halt the staged rollout (stops expansion, does not remove from existing users)
  2. Submit an emergency update immediately — it can go through review in hours if you request it via the priority review flag

App Store. After approval, you can remove the version from sale in App Store Connect before it is downloaded by more users. Users who already have it installed keep it. Submit an expedited update immediately.

The emergency hotfix pipeline:

Detect critical issue
→ Halt rollout (Android) or Remove from sale (iOS)
→ Create hotfix branch from the release tag
→ Fix the specific issue — minimum change
→ Increment build number/version code
→ Run automated tests on the hotfix
→ Submit via CI to Internal track (Android) or TestFlight (iOS)
→ Manual smoke test on physical devices
→ Submit emergency update to production
→ Monitor for 2 hours
→ Resume/expand rollout

The key discipline: the hotfix branch touches only the broken thing. Do not include unrelated changes in an emergency release. Every additional change is a new surface for new bugs.


App Lifecycle Management

Supporting Multiple Versions

Users do not always update immediately. On Android, you can reach 85%+ of your active users within 2 weeks of a release. On iOS, update adoption is typically faster — over 90% within a few weeks for active users. But the tail persists.

Decide your minimum supported version carefully:

  • Set minSdkVersion (Android) and iOS Deployment Target based on your actual user analytics, not the latest available SDK
  • Force-update gates (showing a blocking “please update” screen) should be used only for critical security issues or API incompatibility — users abandon apps that demand updates without clear reason
  • Deprecate old versions gracefully: notify in-app before disabling features, give users a timeline

Privacy and Compliance Updates

Both stores now require ongoing compliance maintenance:

  • Google’s Data Safety section must be updated when you add new SDKs or change data collection practices
  • Apple’s App Privacy declaration must be updated when you add new SDKs — even if your own code does not change
  • GDPR / CCPA consent flows must be present if your app serves EU or California users and uses any advertising or analytics SDKs

A major SDK update (Google Analytics, Firebase, Meta SDK) often changes what data is collected. Check the SDK’s data safety documentation before each update, not just when submitting a new feature.


Publishing a mobile app is a process you learn through iteration. The first submission is always the hardest — every unknown requirement surfaces at once. By the third or fourth release cycle, the process becomes routine: sign, build, upload, monitor, expand, repeat. The developers who do it smoothly are the ones who automated the mechanical parts and internalized the review guidelines well enough that rejections are rare and recoverable when they happen.

The store is not the finish line. It is the beginning of the relationship between your app and its users. Every release is a promise about quality, privacy, and reliability. Keep it.

Tags

#mobile #tutorial #guide #best-practices #tips #devops