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:
- App Store Connect → Users and Access → Integrations → App Store Connect API
- Create a key with App Manager role
- Download the
.p8file - 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
- Google Play Console → Setup → API access
- Link to a Google Cloud project
- Create a service account with Release Manager role
- 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.