TestFlight distribution for Flutter iOS apps
By Charlin Joe · 17 February 2026
TestFlight is Apple's beta testing platform. It lets you distribute your Flutter iOS app to internal testers (up to 25 team members) and external testers (up to 10,000 users) before App Store submission. Here is how to set it up from scratch and automate it with Fastlane.
Prerequisites
- Apple Developer Program membership ($99/year)
- App ID created in App Store Connect
- Provisioning profiles and certificates configured
- Flutter iOS build working locally
Manual upload via Xcode
For occasional uploads, Xcode Organizer is the simplest path:
# Build archive
flutter build ipa --release
# The IPA is at:
# build/ios/ipa/YourApp.ipa
Then open Xcode → Window → Organizer → Distribute App → App Store Connect → Upload.
After upload (usually 5-30 minutes for processing), go to App Store Connect → TestFlight → Build.
Automated with Fastlane
# fastlane/Fastfile
lane :beta do
# Ensure clean git state
ensure_git_status_clean
# Bump build number
increment_build_number(xcodeproj: 'ios/Runner.xcodeproj')
# Build
build_ios_app(
scheme: 'Runner',
workspace: 'ios/Runner.xcworkspace',
configuration: 'Release',
export_method: 'app-store',
export_options: {
provisioningProfiles: {
'com.example.myapp' => 'MyApp Distribution'
}
},
output_directory: './build',
output_name: 'MyApp.ipa',
)
# Upload to TestFlight
upload_to_testflight(
api_key_path: 'fastlane/app_store_connect_api_key.json',
skip_waiting_for_build_processing: true,
ipa: './build/MyApp.ipa',
)
# Commit the build number bump
git_commit(
path: ['ios/Runner.xcodeproj/project.pbxproj'],
message: 'chore: bump iOS build number'
)
end
App Store Connect API key
Using an API key avoids 2FA interruptions in CI:
- App Store Connect → Users and Access → Integrations → App Store Connect API
- Generate a new key with "App Manager" role
- Download the
.p8file (you can only download once)
// fastlane/app_store_connect_api_key.json
{
"key_id": "ABC123DEF",
"issuer_id": "12345678-1234-1234-1234-123456789012",
"key": "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBH...",
"in_house": false
}
In CI, store the contents as a secret and write the file:
- name: Create API key file
run: |
echo '${{ secrets.APP_STORE_CONNECT_API_KEY }}' > fastlane/app_store_connect_api_key.json
Match for certificate management
Fastlane Match stores certificates and provisioning profiles in a private Git repo, making CI certificate management painless:
# Initialize match (first time only)
bundle exec fastlane match init
# Generate/renew distribution certificates
bundle exec fastlane match appstore
lane :beta do
match(
type: 'appstore',
readonly: true, # CI should never regenerate certs
api_key_path: 'fastlane/app_store_connect_api_key.json',
)
build_ios_app(...)
upload_to_testflight(...)
end
GitHub Actions workflow
name: Beta Distribution
on:
push:
branches: [main]
jobs:
testflight:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.22.0'
cache: true
- name: Flutter pub get
run: flutter pub get
- name: Import certificates via Match
run: bundle exec fastlane match appstore --readonly
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
- name: Build and upload to TestFlight
run: bundle exec fastlane beta
env:
APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
Managing testers
Internal testers (up to 25): must be App Store Connect users. Builds are available immediately without Apple review.
External testers (up to 10,000): can be anyone with an email address. Require a one-time beta review by Apple (~24 hours for the first build of a new app, instant for subsequent builds).
Add testers in App Store Connect → TestFlight → [Select build] → Add Testers.
Expiration
TestFlight builds expire after 90 days. If testers are still using a build after 90 days, they're prompted to update. Plan releases so you're not leaving testers on expiring builds.
Collecting feedback
TestFlight users can send feedback via the TestFlight app. Enable in App Store Connect → TestFlight → Settings → Beta App Review Information → User Feedback.
Common pitfalls
Forgetting to increment build number. TestFlight rejects uploads with the same build number as an existing build. Always increment before uploading.
Using 2FA for CI. Apple's 2FA interrupts CI builds. Use the App Store Connect API key (via api_key_path in Fastlane) to avoid this entirely.
Not using readonly: true in CI Match. Match with write access in CI can corrupt your certificates if multiple CI jobs run simultaneously. Always use readonly: true in CI — regenerate certificates manually.
Sign in to like, dislike, or report.