← Articles

Automating App Store submissions for Flutter

By Charlin Joe · 4 February 2026

Fastlane automates App Store and Play Store submissions so you don't click through the console manually. Here is a production-ready setup for Flutter.

Install Fastlane

brew install fastlane
# Or: gem install fastlane

# Initialize in your project root
fastlane init

Select your platform or set up manually (recommended for Flutter since you need both iOS and Android).

Fastfile structure for Flutter

fastlane/Fastfile:

before_all do
  # Ensure we're running from the project root
end

# ===== ANDROID =====

platform :android do
  desc "Build and upload to Play Store Internal Testing"
  lane :internal do
    # Build
    sh "flutter build appbundle \
      --flavor production \
      -t lib/main.dart \
      --obfuscate \
      --split-debug-info=build/debug-info"

    # Upload to Play Store
    upload_to_play_store(
      track: 'internal',
      aab: 'build/app/outputs/bundle/productionRelease/app-production-release.aab',
      package_name: 'com.example.myapp',
      json_key: ENV['PLAY_STORE_JSON_KEY'],
      skip_upload_images: true,
      skip_upload_screenshots: true,
    )
  end

  lane :production do
    # Promote internal to production
    upload_to_play_store(
      track: 'internal',
      track_promote_to: 'production',
      package_name: 'com.example.myapp',
      json_key: ENV['PLAY_STORE_JSON_KEY'],
      rollout: '0.1', # 10% rollout
    )
  end
end

# ===== IOS =====

platform :ios do
  desc "Build and upload to TestFlight"
  lane :beta do
    # Set version from pubspec.yaml
    version = sh("grep 'version:' ../pubspec.yaml | head -1 | awk '{print $2}'").strip
    version_name = version.split('+').first
    build_number = version.split('+').last

    increment_version_number(
      version_number: version_name,
      xcodeproj: 'ios/Runner.xcodeproj',
    )
    increment_build_number(
      build_number: build_number,
      xcodeproj: 'ios/Runner.xcodeproj',
    )

    # Build
    sh "flutter build ipa \
      --flavor production \
      -t lib/main.dart \
      --obfuscate \
      --split-debug-info=build/debug-info \
      --export-options-plist=ios/ExportOptions.plist"

    # Upload to TestFlight
    upload_to_testflight(
      ipa: 'build/ios/ipa/Runner.ipa',
      api_key_path: ENV['APP_STORE_API_KEY_PATH'],
      skip_waiting_for_build_processing: true,
    )
  end

  lane :production do
    upload_to_app_store(
      ipa: 'build/ios/ipa/Runner.ipa',
      api_key_path: ENV['APP_STORE_API_KEY_PATH'],
      submit_for_review: true,
      automatic_release: false,
      force: true, # Skip HTML report
    )
  end
end

App Store Connect API key

Using API keys avoids 2FA issues:

  1. App Store Connect → Users and Access → Integrations → App Store Connect API
  2. Create a key with App Manager role
  3. Download the .p8 file
  4. Save the Issuer ID and Key ID
# Reusable action
app_store_api_key = app_store_connect_api_key(
  key_id: ENV['ASC_KEY_ID'],
  issuer_id: ENV['ASC_ISSUER_ID'],
  key_filepath: ENV['ASC_KEY_PATH'],
)

Google Play JSON key

  1. Google Play Console → Setup → API access
  2. Link to a Google Cloud project
  3. Create a service account with Release Manager role
  4. Download the JSON key file

iOS code signing with Match

match(
  type: 'appstore',
  app_identifier: 'com.example.myapp',
  git_url: 'https://github.com/yourorg/certificates',
  readonly: is_ci, # Don't create new certs on CI
)

Match stores certificates encrypted in a git repo. Everyone on the team (and CI) uses the same certs.

Appfile

fastlane/Appfile:

app_identifier('com.example.myapp')
apple_id('[email protected]')
team_id('YOURTEAMID')
package_name('com.example.myapp')  # Android

Environment variables

fastlane/.env (gitignored):

PLAY_STORE_JSON_KEY=/Users/you/secrets/play-store-key.json
ASC_KEY_ID=XXXXX
ASC_ISSUER_ID=xxxxx-xxxx-xxxx-xxxx-xxxxxxxx
ASC_KEY_PATH=/Users/you/secrets/AuthKey_XXXXX.p8

In CI (GitHub Actions):

env:
  PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}
  ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
  ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}

Running lanes

# Android internal track
bundle exec fastlane android internal

# iOS TestFlight
bundle exec fastlane ios beta

# From GitHub Actions
- name: Deploy to TestFlight
  run: bundle exec fastlane ios beta

Common pitfalls

Not using Bundler. Run Fastlane via bundle exec fastlane (with a Gemfile) to lock the Fastlane version. Without Bundler, different machines run different Fastlane versions and behavior diverges.

Build number conflicts. App Store Connect rejects uploads with a build number already used. Your CI workflow must increment the build number before each upload. Tie it to the pubspec version+build or a CI build counter.

Forgetting --export-options-plist for iOS. flutter build ipa without an export options plist uses development signing by default. Create ios/ExportOptions.plist specifying method: app-store and your team ID, and pass it with --export-options-plist.

Sign in to like, dislike, or report.

Automating App Store submissions for Flutter — ANN Tech