Deploying Flutter apps: a production checklist
By Ann Tech · 19 June 2026
Shipping a Flutter app to the App Store and Play Store involves more than running flutter build. Here is the full checklist — organised by phase — to make sure nothing is missed.
Phase 1: Build configuration
- Version number and build number updated in
pubspec.yaml - Using
--flavor productionand-t lib/main.dartfor the production build -
--obfuscateand--split-debug-info=build/debug-infoenabled -
--tree-shake-iconsenabled to remove unused Material icons - No debug flags or dev tools in production build (
kDebugModeguards checked) - Production Firebase project connected (not dev/staging)
- Production API base URL configured via
--dart-definefrom CI secrets
flutter build appbundle \
--flavor production \
-t lib/main.dart \
--obfuscate \
--split-debug-info=build/debug-info \
--tree-shake-icons \
--dart-define=ENVIRONMENT=production \
--dart-define=API_BASE_URL=$PROD_API_URL
Phase 2: Android signing and Play Store
- Release keystore exists and is backed up securely (not just on one developer's laptop)
-
build.gradle.ktsreferences signing config from environment variables (not hardcoded) - App Bundle (
.aab) uploaded, not APK — Play Store requires AAB - Target SDK version is current (Google requires targeting within ~1 year of latest)
- Permissions in
AndroidManifest.xmlmatch what the app actually uses - App name, short description, full description updated in Play Console
- Screenshots uploaded for phone, tablet, and Chromebook (if applicable)
- Privacy policy URL added to Play Console
- Content rating questionnaire completed
Phase 3: iOS signing and App Store
- Provisioning profile is App Store distribution type (not AdHoc or Development)
- Bundle ID matches App Store Connect
-
Info.plisthas usage description strings for every permission requested (NSCameraUsageDescription, etc.) -
ExportOptions.plistspecifiesmethod: app-storeand correct team ID - App version and build number incremented in Xcode /
pubspec.yaml - Privacy manifest (
PrivacyInfo.xcprivacy) added if using required reason APIs - App Store Connect metadata updated: screenshots, description, keywords, what's new
- Age rating set correctly
Phase 4: Monitoring and crash reporting
- Firebase Crashlytics (or Sentry) initialised in
main()with the uncaught error handler - Debug symbols uploaded to Crashlytics / Sentry after build (
upload-symbolsscript) - Performance monitoring enabled (Firebase Performance or custom)
- Analytics events verified with Firebase DebugView before shipping
void main() async {
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
await Firebase.initializeApp();
runApp(const MyApp());
}
Phase 5: Pre-release testing
- TestFlight / internal testing track tested by QA team
- Critical user flows tested on a real device (not simulator)
- Dark mode tested
- Accessibility: TalkBack/VoiceOver passes through main flows
- Offline behaviour tested (airplane mode)
- Deep links tested from external app and browser
- Push notifications tested end-to-end (sent from server, received in all app states)
- App size measured with
--analyze-sizeand within acceptable range
Phase 6: Rollout strategy
- Phased rollout configured (Play Store: start at 10%; App Store: phased release)
- Rollout monitoring plan: watch crash rate, ANR rate, and key metrics for 24-48 hours
- Rollout halt criteria defined: halt if crash rate > X% or revenue drops > Y%
- Previous release build retained in case rollback is needed
Play Store rollout: 10% → 25% → 50% → 100% over 3-5 days
App Store phased: 1% → 2% → 5% → 10% → 20% → 50% → 100% over 7 days
Phase 7: Post-release
- Crashlytics / Sentry dashboard checked 1 hour after release
- Store reviews monitored for new issues
- Debug symbols retained for the shipped build version (needed for symbolication)
Automating with Fastlane
# fastlane/Fastfile
lane :release do
# Run tests first
sh "flutter test"
# Android
sh "flutter build appbundle --flavor production -t lib/main.dart --obfuscate --split-debug-info=build/debug-info"
upload_to_play_store(track: 'internal', aab: 'build/app/outputs/bundle/productionRelease/app-production-release.aab')
# iOS
sh "flutter build ipa --flavor production -t lib/main.dart --export-options-plist=ios/ExportOptions.plist"
upload_to_testflight(ipa: 'build/ios/ipa/Runner.ipa', skip_waiting_for_build_processing: true)
# Tag the release
sh "git tag v#{flutter_version_name}"
sh "git push origin --tags"
end
Common pitfalls
Shipping the wrong Firebase config. Using the dev google-services.json in a production build means prod users write to your dev Firestore. Always verify the Firebase project ID in the build: log it on startup in staging, and check that kDebugMode guards prevent any dev config from reaching production.
Not uploading debug symbols. Obfuscated crash reports are unreadable without symbols. Upload them to Crashlytics/Sentry immediately after every release build — the symbols must match the exact build, so you can't retroactively upload them later.
Skipping phased rollout. Releasing to 100% of users immediately means a bug introduced in this version affects everyone instantly with no containment. Phased rollouts cap the blast radius to a fraction of users while you monitor.
Sign in to like, dislike, or report.