Release Engineering Guide

Multi-Platform Deployment Guide

A step-by-step path to ship a Flutter app to the Apple App Store and Google Play, from a working project through store review, including the extra steps that in-app purchases and sensitive data add.

Platforms: iOS and Android from one Flutter codebase Assumes: Apple Developer Program and Google Play Console accounts already active Method: Manual, both platforms
Start Here

Overview and release model#

One Flutter codebase produces two native apps. You build an iOS app for the App Store and an Android app bundle for Google Play, then submit each through its own console. The Dart code stays shared. The store work is platform specific, and that is what this guide covers.

This guide assumes the manual path with no CI system. You build on a Mac, upload through Apple and Google tooling, and press submit in each console. Once the first release is out, you can automate later.

It also assumes the accounts already exist. You are enrolled in the Apple Developer Program with access to App Store Connect, and you have a verified Google Play Console developer account. Account enrollment and identity verification are not covered here.

Decide these before you start. A reverse-domain application identifier, the same on both platforms where possible, for example com.example.yourapp. A minimum iOS version and an Android minSdk. Whether the app is free, paid, or free with in-app purchases. Which device permissions the app needs. These choices show up in project files, store forms, and review notes, so settle them once.
TrackAppleGoogle
ConsoleApp Store ConnectGoogle Play Console
Build artifact.ipa from flutter build ipa.aab from flutter build appbundle
SigningXcode automatic signing with your teamYour upload key plus Play App Signing
Pre-release testingTestFlightInternal and closed testing tracks
Privacy formsApp Privacy labelsData safety, plus category declarations
Typical first review1 to 3 days, longer for sensitive categoriesUp to 7 days, longer for sensitive categories

Before you start#

Accounts and agreements

Confirm these are in place. Each one blocks a later step if it is missing.

  • Apple Developer Program membership is active, and your Apple ID has the Admin or App Manager role in App Store Connect.
  • If you sell anything in-app, the Paid Applications Agreement is signed in App Store Connect under Business, with banking and tax details entered. In-app purchases do not work until this is active.
  • Google Play Console account is verified, and your Google account has permission to create apps and manage releases.
  • If you sell anything in-app, a Google Payments profile is linked to the Play account.

Legal and policy pages

Host these at stable public URLs before you submit. Both stores ask for them and reviewers open them.

  • Privacy policy. Both stores require one for every app, even apps that collect nothing. It must match the answers you give in the App Privacy and Data safety forms.
  • Terms of service, if the app has accounts or purchases.
  • A support contact URL or email, and a marketing or support website.

Store assets

  • App icon at 1024 by 1024 for Apple and 512 by 512 for Google, no alpha channel, no rounded corners baked in.
  • Screenshots for the required device sizes. Apple wants a 6.9 inch iPhone set, plus iPad if the app supports it. Google wants phone screenshots, plus tablet sets if you target tablets.
  • A feature graphic at 1024 by 500 for Google Play.
  • Short and full descriptions, and a subtitle or promotional text where the store allows one. Avoid claims you cannot back up.

Tooling on your Mac

flutter --version
flutter doctor -v

You want green entries for Flutter, the Android toolchain, Xcode, and CocoaPods. Fix anything missing before continuing, and confirm the app runs cleanly on both a simulator and an emulator.

flutter clean
flutter pub get
flutter analyze
flutter test
Decide your version now. The first public release is usually 1.0.0+1 in pubspec.yaml. The part before the plus is the user-facing version. The number after the plus is the build number. Every upload to either store needs a higher build number than the last, even for a rejected build you resubmit.
Part A · Apple App Store

A1. Register the App ID and capabilities#

Register the identifier and turn on the capabilities the app uses. These must match what the app declares, or the upload fails.

  1. Open Certificates, Identifiers and Profiles and create a new App ID. Choose Explicit and enter your bundle identifier, for example com.example.yourapp.
  2. Enable only the capabilities the app needs, for example Push Notifications, Sign in with Apple, Background Modes, Associated Domains, or HealthKit. In-App Purchase is on for every App ID by default.
  3. Register at least one device UDID if you plan to run release builds on hardware before TestFlight. TestFlight itself does not need this.
  4. Let Xcode manage certificates and profiles automatically. Manual certificates are only worth it once you move to CI.
Match the app record. Whatever you enable here has to match the entitlements in the Xcode project and the App Privacy answers later. A capability that is on in the App ID but missing from the project, or the reverse, is rejected at upload or review.

A2. Configure the Xcode project#

Open the workspace, not the project file. The workspace includes the CocoaPods dependencies that Flutter plugins rely on.

open ios/Runner.xcworkspace

Signing and identity

Select the Runner target, then Signing and Capabilities.

  • Turn on Automatically manage signing.
  • Select your team.
  • Confirm the bundle identifier matches the App ID you registered.
  • Confirm the capabilities you enabled in A1 appear here. Add any that are missing and remove any you do not use.
  • Under Deployment, set the minimum iOS version. Check the floor your plugins require before lowering it.

Privacy purpose strings

Apple rejects apps that request device access without a clear reason. Add a plain-language usage string to ios/Runner/Info.plist for every permission the app requests. Write them from the user's point of view, and say what the app does with the data.

<key>NSCameraUsageDescription</key>
<string>Take photos to attach to your entries. Photos stay
on your device unless you choose to share them.</string>

<key>NSPhotoLibraryUsageDescription</key>
<string>Pick existing photos to attach to your entries.</string>

<key>NSLocationWhenInUseUsageDescription</key>
<string>Show nearby results while you are using the app.</string>

Common keys include NSCameraUsageDescription, NSPhotoLibraryUsageDescription, NSMicrophoneUsageDescription, NSLocationWhenInUseUsageDescription, NSContactsUsageDescription, NSHealthShareUsageDescription, and NSHealthUpdateUsageDescription. A missing key terminates the app at the moment it asks, which reviewers will hit.

Request in context. Ask for each permission when the related feature is used, not through one blanket request at launch. Reviewers test this, and a wall of prompts on first open is a common rejection reason.

Privacy manifest

Apple requires a PrivacyInfo.xcprivacy file that lists the data your app collects and the reasons for any required-reason APIs it calls. Most Flutter plugins ship their own. Add one to the Runner target for anything the app itself collects.

Icons and version

Set the app icons in Assets.xcassets inside the Runner folder. Confirm the version in pubspec.yaml reads 1.0.0+1. Flutter maps the name to CFBundleShortVersionString and the build number to CFBundleVersion.

References: Flutter iOS deployment and Apple privacy manifest files.

A3. Create the app record and in-app purchases#

The app record must exist before you can upload a build. Create it now, then add products if the app sells anything.

  1. In App Store Connect, open Apps, click the plus, and choose New App. Select iOS, set the name, primary language, bundle ID, and a SKU of your choosing, then create.
  2. Skip the rest of this section if the app is free with no purchases, or is sold at a one-time price on the store.
  3. Open In-App Purchases or Subscriptions, depending on your model. For subscriptions, create a subscription group, then add each plan inside it, for example a monthly and an annual.
  4. Set prices per plan. Add an introductory offer if you want a free trial or a discounted first period.
  5. Add a localized display name, description, and a review screenshot for each product. Apple will not approve a product without them.
  6. Validate purchases on your backend or with StoreKit 2 on device, and store the entitlement in a platform-independent form so a user who buys on iOS is recognized on Android too.
  7. Create a Sandbox Apple ID under Users and Access and test the full purchase and restore flow before you submit.
First-release products are reviewed with the app. Apple reviews new in-app purchases alongside the first build that uses them, so the products must be complete and attached to the version when you submit. Mirror the same plan structure and prices on Google Play so entitlement logic and support answers stay the same on both platforms.

Reference: Apple StoreKit.

A4. Build the IPA#

Build a signed release archive from the command line.

flutter clean
flutter pub get
flutter build ipa --build-name=1.0.0 --build-number=1

To protect the Dart code, add obfuscation.

flutter build ipa --obfuscate --split-debug-info=build/ios/symbols \
  --build-name=1.0.0 --build-number=1

This produces an archive under build/ios/archive/ and an .ipa under build/ios/ipa/. Keep the symbols folder. You need it to read crash reports later.

Save your symbols. If you obfuscate, store the build/ios/symbols output somewhere safe for every release. Without it, crash logs from that build are unreadable.

A5. Upload the build and test on TestFlight#

  1. Install Apple Transporter from the Mac App Store and sign in with the Apple ID on your developer team.
  2. Drag the .ipa from build/ios/ipa/ into Transporter and deliver it. You can also open the archive in Xcode and use Validate App, then Distribute App, which catches signing and entitlement problems before delivery.
  3. Wait for the processing email, usually under 30 minutes. Apple runs an automated pass first and emails you about missing purpose strings or export compliance.
  4. Answer the export compliance question. Most apps that only use HTTPS qualify for the exemption. Setting ITSAppUsesNonExemptEncryption to false in Info.plist stops the question repeating on every build.
  5. Open the TestFlight tab, add the build to Internal Testing, and invite your team. Internal testers do not need a separate Apple review.
  6. Run the flows reviewers probe on a real device: every permission prompt, sign-in and sign-out, purchase in sandbox, restore purchases, and account deletion.
External beta. To reach testers outside your team, add an External Testing group in TestFlight. External groups need a light Apple review of the first build, which is quick compared to full App Store review, and can hold up to 10,000 testers.

A6. Store listing, App Privacy, and age rating#

Fill in the App Store product page and the privacy and rating sections.

App Privacy labels

In the App Privacy section, declare every type of data the app collects, whether it is linked to the user, and whether it is used for tracking. Count data your third-party SDKs collect too, such as analytics and crash reporting. If the app tracks users across other companies' apps or sites, it must show the App Tracking Transparency prompt. If it does not, say so and keep the answers consistent.

Age rating

Answer the age-rating questionnaire honestly. It drives the rating shown on the listing and which regions can see the app. If the app is for adults only, set the minimum age in availability where offered.

Required URLs and account deletion

  • Enter the privacy policy URL and a support URL.
  • If the app lets users create an account, it must also let them delete it inside the app. Confirm the flow works and is easy to find, because Apple checks it.
  • Complete Pricing and Availability, including which countries to launch in.

References: App privacy details and Offering account deletion in your app.

A7. Submit for App Store review#

  1. Attach the processed build to the version.
  2. In App Review Information, add a demo account if the app needs sign-in, sandbox purchase steps so the reviewer can test paid features, and notes that explain anything unusual.
  3. Describe plainly what the app does and does not do. If it touches a regulated area such as health, finance, or gambling, state its boundaries up front.
  4. Choose manual or automatic release after approval. Manual release lets you line up the Android rollout on the same day.
  5. Submit for review.
Plan for extra time. Most first submissions take one to three days, but a rejection and resubmission can add a week. Do not promise a hard ship date before the first approval.

Reference: Apple App Review Guidelines.

Part B · Google Play

B1. Set up app signing#

Google Play signs the app your users download. You sign your uploads with an upload key. Create that key once and keep it safe. Losing it means a support request to Google to reset it and a delay in shipping updates.

  1. Create an upload keystore.
    keytool -genkey -v -keystore ~/upload-keystore.jks \
      -keyalg RSA -keysize 2048 -validity 10000 -alias upload
  2. Create android/key.properties. Do not commit it.
    storePassword=your-store-password
    keyPassword=your-key-password
    keyAlias=upload
    storeFile=/Users/yourname/upload-keystore.jks
  3. Wire the signing config into android/app/build.gradle.kts, loading the properties before the android block and referencing them in a release signing config.
    import java.util.Properties
    import java.io.FileInputStream
    
    val keystoreProperties = Properties()
    val keystorePropertiesFile = rootProject.file("key.properties")
    if (keystorePropertiesFile.exists()) {
        keystoreProperties.load(FileInputStream(keystorePropertiesFile))
    }
    
    android {
        signingConfigs {
            create("release") {
                keyAlias = keystoreProperties["keyAlias"] as String
                keyPassword = keystoreProperties["keyPassword"] as String
                storeFile = keystoreProperties["storeFile"]?.let { file(it) }
                storePassword = keystoreProperties["storePassword"] as String
            }
        }
        buildTypes {
            release {
                signingConfig = signingConfigs.getByName("release")
            }
        }
    }
Guard the keystore and passwords. Store upload-keystore.jks and key.properties outside the repository, in a password manager or secret store, and add both to .gitignore. Back them up in two places.

References: Flutter Android deployment and Play App Signing.

B2. Configure the Android project#

Application ID and SDK levels

In android/app/build.gradle.kts, confirm the identity and SDK levels.

applicationId = "com.example.yourapp"
minSdk = 24
compileSdk = 36
targetSdk = 36

Google requires new apps and updates to target a recent API level, so keep targetSdk at the latest stable release. Set minSdk as low as your plugins allow and no lower than you will test. The application ID cannot change after the first upload.

Permissions

Declare in AndroidManifest.xml only the permissions tied to visible features. Google reviews every sensitive permission against a feature it can see, and some, such as SMS, call log, all-files access, and health data, need a declaration form and a written or video justification.

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- add only the permissions the app actually uses -->
Request only what you use. Extra permissions with no matching feature cause rejection. Keep the list tight and be ready to justify each one in the Data safety form.

Icon and version

Replace the launcher icons under android/app/src/main/res/, or generate them with the flutter_launcher_icons package. Flutter maps the build name in pubspec.yaml to versionName and the build number to versionCode.

Reference: Play target API level requirement.

B3. Build the app bundle#

Build a signed Android App Bundle. Google Play requires bundles for new apps.

flutter clean
flutter pub get
flutter build appbundle --build-name=1.0.0 --build-number=1

With obfuscation:

flutter build appbundle --obfuscate --split-debug-info=build/android/symbols \
  --build-name=1.0.0 --build-number=1

The bundle lands at build/app/outputs/bundle/release/app-release.aab. Every upload needs a higher versionCode than the last. Keep the symbols folder for the same reason as on iOS.

B4. Create the app and run internal testing#

  1. In Play Console, choose Create app. Set the name, default language, app or game, and free or paid. A free app cannot be changed to paid later.
  2. Open Testing, then Internal testing. Create a release, upload app-release.aab, and accept Play App Signing when prompted. Google generates and holds the app signing key. You keep signing with your upload key.
  3. Add your team as internal testers with their Google accounts and share the opt-in link. Internal testing supports up to 100 testers and needs no review.
  4. Install through the link and run the flows reviewers probe: every permission prompt, sign-in, purchase, restore, and account deletion.
  5. When internal testing looks good, promote the same release to a Closed testing track for a wider beta.
Check whether the 12-tester rule applies to you. Personal developer accounts created after November 13, 2023 must run a closed test with at least 12 testers opted in for 14 continuous days, then apply for production access, before they can publish. Organization accounts and older personal accounts are exempt. If the rule applies, start the closed test as early as possible because it is the longest fixed wait in the process.

Reference: Play testing requirements for new personal accounts.

B5. Set up in-app purchases#

Skip this if the app sells nothing. Play Console only lets you create products after a bundle that includes the Play Billing library has been uploaded, which is why this comes after internal testing.

  1. Open Monetize with Play, then Products, then In-app products or Subscriptions.
  2. For subscriptions, create one subscription and add a base plan for each billing period, for example monthly and annual. Prices are set per base plan.
  3. Add an offer, such as a free trial or introductory price, on any plan that needs one.
  4. Match product identifiers and prices to the Apple products so entitlement logic stays the same on both platforms.
  5. Confirm your backend validates Play purchases and writes to the same entitlement record used on iOS.
  6. Add license-test accounts under Setup, then License testing, so test purchases are not charged. Then run the purchase and restore flow on a device installed from the internal testing link.

Reference: Google Play Billing.

B6. Store listing and declarations#

Complete every item under App content before you request production. The Dashboard shows what is left.

Main store listing

Add the icon, feature graphic, screenshots, short description, and full description. Keep claims conservative and consistent with what the app does.

Data safety

Declare what the app collects, how it is used, whether it is shared, whether users can request deletion, and how it is secured. Include data collected by SDKs. Your answers must line up with the privacy policy and with the App Privacy labels you gave Apple.

Declarations every app completes

  • Privacy policy URL.
  • Ads. Say whether the app shows ads.
  • Content rating questionnaire, which produces the rating shown on the listing.
  • Target audience and content. If the app targets children, extra Families policy rules apply.
  • News, government, financial features, and health apps declarations. Every app answers them, even to say the category does not apply.
  • Account deletion. If users can create an account, provide an in-app deletion path and a web URL for deletion requests.
Sensitive permissions need their own form. If the manifest requests SMS, call log, all-files access, accessibility services, exact alarms, or health data, Play shows an additional declaration asking which feature needs it. Health apps also go through a data-type review before Health Connect access works. See the sensitive-data section below.

Reference: Data safety form.

B7. Roll out to production#

  1. Confirm every section under App content shows complete and the Dashboard has no open tasks.
  2. If the 12-tester rule applied, apply for production access after the 14 days and wait for approval.
  3. Open Production, create a release, and promote the tested bundle from a testing track rather than uploading a fresh untested build.
  4. Choose the countries to release in, then use a staged rollout. Start at a small percentage so you can watch crash-free rate on real devices before reaching everyone.
  5. Send for review. Google says up to seven days for new apps, and sensitive categories can take longer.
  6. Once live, raise the rollout percentage in steps, and halt the rollout from the console if a crash spike appears.
Wrap Up

Extra work for sensitive-data apps#

Apps that touch health, finance, children, or other regulated areas get a stricter review on both stores. The underlying commitments are the same in both consoles, so get them right once. Health apps are the most common case and carry the most specific rules.

RequirementWhat reviewers check
No unsupported claimsApp and listing describe what the app does without promising medical, financial, or legal outcomes. Health apps must not read as diagnosis, dosing, or treatment advice.
Purpose strings and scoped permissionsEvery sensitive permission maps to a visible feature and explains itself in plain language. Health apps declare only the data types they use.
Privacy policy live and consistentPublic URL, no sale of sensitive data, no sensitive data for ads, matches the App Privacy and Data safety answers.
In-app account deletionReachable inside the app, ideally offers an export first, confirms completion.
Analytics exclude sensitive valuesNo health readings, balances, or personal notes in analytics events.
Organization account where requiredSome health distributions on Play require an organization account. Check the policy for your category before you build.

Health apps specifically

  • Apple: enable HealthKit on the App ID and in Xcode, add NSHealthShareUsageDescription and NSHealthUpdateUsageDescription, and state in review notes that the app is a wellness or recordkeeping tool.
  • Google: Health Connect requires Android 9 or newer, so set minSdk 28. Declare each Health Connect data type in the manifest, complete the Health apps declaration, and pass the Health Connect data-type review before access works. The Play privacy-policy URL must match the one Health Connect shows.
  • Both: some US states have consumer-health privacy laws, such as the Washington My Health My Data Act, that require a separate consumer-health privacy notice.
The fastest way to get rejected. Wording that reads as professional advice, a permission with no matching feature, or a privacy answer that contradicts your policy. Sweep the app and both listings for these before you submit.

References: Setting up HealthKit, Health apps declaration form, Publish a Health Connect app, and Google Play health apps policy.

Timeline and updates#

Rough sequence for a first release with accounts already in place. Testing and store review are the long poles, so start uploading builds early.

PhaseTypical durationNotes
Project config and first builds1 to 3 daysSigning, capabilities, purpose strings, products.
Internal and closed testing1 to 3 weeks14 days minimum if the Play 12-tester rule applies to your account.
Listings and declarations1 to 2 daysFaster if assets and policy pages were prepared up front.
Store review, first submissionSeveral days to a few weeksSensitive categories can take longer. Keep a two to four week buffer before any public launch date.

Shipping updates later

For each new release, bump the version in pubspec.yaml, rebuild both artifacts, upload, test on a track, then promote. The one-time work in this guide, signing keys, capabilities, products, and declarations, does not repeat, though both stores ask you to reconfirm the privacy forms when your data practices change. Once the manual flow is steady, a tool like Fastlane or Codemagic can automate the build and upload steps.

Master submission checklist#

Shared, do once

  • Apple Developer Program and Play Console accounts active, with paid agreements and payment profiles if selling in-app
  • Application identifier chosen and identical on both platforms
  • Privacy policy and terms live at public URLs
  • Support URL and marketing site ready
  • Icon, screenshots, feature graphic, and descriptions prepared
  • In-app account deletion confirmed working if the app has accounts
  • Version set to 1.0.0+1 and a plan for bumping build numbers

Apple App Store

  • App ID registered with only the capabilities the app uses
  • Xcode signing, capabilities, purpose strings, and privacy manifest set
  • App record created and in-app products configured, if any
  • IPA built with symbols saved, uploaded via Transporter, tested on TestFlight
  • App Privacy labels, age rating, and URLs complete
  • Review notes include demo account and purchase test steps
  • Submitted for review

Google Play

  • Upload keystore created, key.properties and Gradle signing configured, keystore backed up
  • applicationId, SDK levels, and scoped permissions set
  • App bundle built with symbols saved
  • App created, Play App Signing accepted, internal testing run on real devices
  • In-app products configured and test purchases made, if any
  • Data safety, content rating, target audience, ads, and category declarations complete
  • Closed test completed if the 12-tester rule applies
  • Promoted to production with a staged rollout and sent for review