Automate Your Flutter iOS Deployment with Fastlane

Gautier Siclon
Gautier Siclon

Co-founder, Apparence.io

Published on
Automate Flutter iOS Deployment with Fastlane

If you have ever shipped a Flutter app to the App Store, you know the pain.

Open Xcode. Wait for indexing. Archive. Wait again. Upload. Fill in release notes manually for every locale. Click through 15 screens. Hope nothing breaks.

Now imagine doing that every week. Or every time you fix a critical bug.

There is a better way. Fastlane lets you automate the entire iOS deployment process with a single terminal command. No Xcode GUI, no manual uploads, no copy-pasting release notes.

In this guide, I will walk you through a real Fastlane setup that I use to deploy my own Flutter apps. Not a toy example. A production config that handles API authentication, localized metadata, and App Store uploads.


Why Fastlane for Flutter iOS deployment

Flutter builds your app. Fastlane ships it.

Flutter's flutter build ipa gives you a binary. But getting that binary to the App Store with the right metadata, release notes, and screenshots is a whole separate job. That is where Fastlane comes in.

Here is what Fastlane handles for you:

  • Authentication with App Store Connect (no 2FA prompts)
  • Metadata management — descriptions, keywords, release notes for every locale
  • Binary uploads — push your IPA directly to App Store Connect
  • Version creation — create new app versions programmatically
  • Submission — optionally submit for review automatically

All from one command in your terminal.


Prerequisites

Before we start, make sure you have:

  1. A Flutter app that builds successfully with flutter build ipa
  2. An Apple Developer account with App Store Connect access
  3. Ruby installed (macOS comes with it, or use rbenv)
  4. Fastlane installed:
gem install fastlane

Then initialize Fastlane in your Flutter project's ios/ directory:

cd ios
fastlane init

Choose option 4 (manual setup) when prompted. This creates the ios/fastlane/ directory with a Fastfile and Appfile.


Step 1 — Set up App Store Connect API key

The first thing you want to do is stop using your Apple ID to authenticate. Apple's 2FA will block your automation every time.

Instead, create an App Store Connect API key:

  1. Go to App Store Connect > Users and Access > Integrations > App Store Connect API
  2. Click the + button to generate a new key
  3. Give it a name like "Fastlane CI" and select the App Manager role
  4. Download the .p8 key file — you can only download it once

Now store these values as environment variables. Add them to your .zshrc, .bashrc, or CI secrets:

export FASTLANE_APP_STORE_CONNECT_API_KEY_ID="YOUR_KEY_ID"
export FASTLANE_APP_STORE_CONNECT_API_ISSUER_ID="YOUR_ISSUER_ID"
export FASTLANE_APP_STORE_CONNECT_API_KEY_PATH="/path/to/AuthKey.p8"

In your Fastfile, create a helper function that loads this key:

def load_api_key
  app_store_connect_api_key(
    key_id: ENV["FASTLANE_APP_STORE_CONNECT_API_KEY_ID"],
    issuer_id: ENV["FASTLANE_APP_STORE_CONNECT_API_ISSUER_ID"],
    key_filepath: ENV["FASTLANE_APP_STORE_CONNECT_API_KEY_PATH"],
    in_house: false,
  )
end

Every lane calls load_api_key first. No passwords, no 2FA, no session tokens that expire.


Step 2 — Organize your metadata

Fastlane uses a folder structure to manage your App Store metadata. Each locale gets its own folder with text files for the different fields.

fastlane/metadata/
  en-US/
    release_notes.txt
    description.txt
    keywords.txt
  fr-FR/
    release_notes.txt
    description.txt
  de-DE/
    release_notes.txt
  ...

You can bootstrap this structure by pulling your existing metadata from App Store Connect:

desc "Download metadata from App Store Connect"
lane :download_app_metadata do
  load_api_key
  deliver(
    download_metadata: true,
    download_screenshots: false,
    force: true,
  )
end

Run it with:

fastlane download_app_metadata

This downloads all your current descriptions, keywords, and release notes into the metadata/ folder. Now you can edit them locally and push updates without touching App Store Connect.

Handling localized release notes

If you support multiple languages, you need release notes for each locale. Here is a helper that loads them all, with a fallback to English:

def load_release_notes(metadata_path = nil)
  metadata_path ||= File.join(__dir__, "metadata")
  fallback = File.read(File.join(metadata_path, 'en-US/release_notes.txt'))
  locales = %w[da de-DE en-CA en-US es-ES fr-FR he it ja ko nl-NL
               no pl pt-BR pt-PT sv tr zh-Hans zh-Hant ar-SA hu]
  locales.each_with_object({}) do |locale, hash|
    file = File.join(metadata_path, locale, 'release_notes.txt')
    hash[locale] = File.exist?(file) ? File.read(file) : fallback
  end
end

This is important. If you forget a locale, Apple will reject your submission or show stale release notes. This helper makes sure every supported locale has content.


Step 3 — Create the "release new version" lane

This lane creates a new app version on App Store Connect and pushes your metadata — without uploading a binary. This is useful when you want to update release notes, descriptions, or keywords independently.

desc "Create a new version and push metadata"
lane :release_new_version do |options|
  load_api_key

  produce(
    app_identifier: "com.yourcompany.yourapp",
    app_version: options[:version],
  )

  release_notes = load_release_notes('./metadata')

  deliver(
    app_version: options[:version],
    skip_binary_upload: true,
    force: true,
    submit_for_review: false,
    automatic_release: false,
    metadata_path: "./fastlane/metadata",
    release_notes: release_notes,
    precheck_include_in_app_purchases: false,
    skip_screenshots: true,
  )
end

Run it:

fastlane release_new_version version:"1.2.0"

One command. New version created. Metadata pushed. Release notes in 20+ languages. Done.


Step 4 — Create the deploy lane

This is the main lane. It takes your Flutter IPA, uploads it to App Store Connect, and optionally submits for review.

First, build your Flutter app:

flutter build ipa --release

This generates an IPA file in build/ios/ipa/. Now the deploy lane picks it up:

desc "Build and deploy app to App Store Connect"
lane :deploy do |options|
  load_api_key
  get_push_certificate

  project_root = File.expand_path("../../", __dir__)
  ipa_path = Dir.glob(
    File.join(project_root, "build/ios/ipa/*.ipa")
  ).first

  if ipa_path.nil?
    UI.user_error!("Could not find IPA file.")
  end

  UI.success("Found IPA at: #{ipa_path}")

  release_notes = options[:version] ? load_release_notes : nil

  upload_to_app_store(
    ipa: ipa_path,
    skip_metadata: true,
    skip_screenshots: true,
    force: true,
    submit_for_review: options[:submit_for_review] || false,
    automatic_release: options[:automatic_release] || false,
    metadata_path: "./fastlane/metadata",
    release_notes: release_notes,
    precheck_include_in_app_purchases: false,
    submission_information: {
      export_compliance_uses_encryption: false,
      export_compliance_encryption_updated: false,
    },
  )
end

Deploy without submitting for review:

fastlane deploy

Deploy and submit for review in one shot:

fastlane deploy submit_for_review:true

The submission_information block handles Apple's export compliance questions automatically. No more clicking through those dialogs.


The full Fastfile

Here is the complete Fastfile putting it all together:

default_platform(:ios)

platform :ios do
  def load_api_key
    app_store_connect_api_key(
      key_id: ENV["FASTLANE_APP_STORE_CONNECT_API_KEY_ID"],
      issuer_id: ENV["FASTLANE_APP_STORE_CONNECT_API_ISSUER_ID"],
      key_filepath: ENV["FASTLANE_APP_STORE_CONNECT_API_KEY_PATH"],
      in_house: false,
    )
  end

  def load_release_notes(metadata_path = nil)
    metadata_path ||= File.join(__dir__, "metadata")
    fallback = File.read(
      File.join(metadata_path, 'en-US/release_notes.txt')
    )
    locales = %w[da de-DE en-CA en-US es-ES fr-FR he it ja ko
                 nl-NL no pl pt-BR pt-PT sv tr zh-Hans zh-Hant
                 ar-SA hu]
    locales.each_with_object({}) do |locale, hash|
      file = File.join(metadata_path, locale, 'release_notes.txt')
      hash[locale] = File.exist?(file) ? File.read(file) : fallback
    end
  end

  lane :download_app_metadata do
    load_api_key
    deliver(
      download_metadata: true,
      download_screenshots: false,
      force: true,
    )
  end

  lane :release_new_version do |options|
    load_api_key
    produce(
      app_identifier: "com.yourcompany.yourapp",
      app_version: options[:version],
    )
    release_notes = load_release_notes('./metadata')
    deliver(
      app_version: options[:version],
      skip_binary_upload: true,
      force: true,
      submit_for_review: false,
      automatic_release: false,
      metadata_path: "./fastlane/metadata",
      release_notes: release_notes,
      precheck_include_in_app_purchases: false,
      skip_screenshots: true,
    )
  end

  lane :deploy do |options|
    load_api_key
    get_push_certificate
    project_root = File.expand_path("../../", __dir__)
    ipa_path = Dir.glob(
      File.join(project_root, "build/ios/ipa/*.ipa")
    ).first
    if ipa_path.nil?
      UI.user_error!("Could not find IPA file.")
    end
    release_notes = options[:version] ? load_release_notes : nil
    upload_to_app_store(
      ipa: ipa_path,
      skip_metadata: true,
      skip_screenshots: true,
      force: true,
      submit_for_review: options[:submit_for_review] || false,
      automatic_release: options[:automatic_release] || false,
      metadata_path: "./fastlane/metadata",
      release_notes: release_notes,
      precheck_include_in_app_purchases: false,
      submission_information: {
        export_compliance_uses_encryption: false,
        export_compliance_encryption_updated: false,
      },
    )
  end
end

My typical deploy workflow

Here is what a release looks like in practice:

# 1. Build the Flutter app
flutter build ipa --release

# 2. Update release notes in metadata/en-US/release_notes.txt
# (and other locales if needed)

# 3. Deploy to App Store Connect
cd ios
fastlane deploy version:"1.2.0" submit_for_review:true

Three commands. That is it. No Xcode, no web browser, no manual form filling.

What used to take 20-30 minutes of clicking through App Store Connect now takes under 2 minutes of actual work. The upload itself still takes a few minutes depending on your binary size, but you are free to do something else while it runs.


Tips and common pitfalls

Store your API key securely

Never commit the .p8 file to git. Add it to .gitignore and use environment variables or your CI's secret store.

Use force: true carefully

The force: true flag skips Fastlane's HTML report preview. This is fine for CI, but when running locally for the first time, remove it to double-check what Fastlane will push.

Keep metadata in version control

Your metadata/ folder should be committed to git. This gives you a history of every release note change and makes it easy for translators to submit PRs.

Handle missing locales gracefully

The load_release_notes helper above falls back to English for any missing locale. This prevents submission failures when you add a new language to your app but haven't translated the release notes yet.

Export compliance

The submission_information block in the deploy lane answers Apple's export compliance questions automatically. If your app uses encryption beyond standard HTTPS, you will need to adjust these values.


Stop deploying manually

Every minute you spend clicking through Xcode and App Store Connect is a minute you are not building features or fixing bugs.

Fastlane is a one-time setup that pays for itself on every single release. If you are shipping a Flutter app to the App Store, this is not optional. It is infrastructure.

Set it up once. Deploy with one command. Move on to the work that actually matters.

If you want to skip the boilerplate entirely and start with a Flutter project that already includes deployment automation, authentication, subscriptions, and everything else you need to ship, check out ApparenceKit. It is built for developers who want to ship fast and focus on their product.

Build your Flutter app 10x faster

One command. Pick your modules. Firebase or Supabase auto-configured. Start building what matters.

kickstarter for flutter apps

Frequently Asked Questions

What is Fastlane and why should I use it for Flutter?

Fastlane is an open-source automation tool that handles tedious iOS and Android deployment tasks like code signing, building, uploading to the App Store, and managing metadata. For Flutter developers, it eliminates the need to manually open Xcode, archive, and upload every time you release an update.

How do I avoid 2FA prompts when using Fastlane?

Use an App Store Connect API key instead of your Apple ID credentials. Generate a key in App Store Connect under Users and Access > Integrations > App Store Connect API, then reference it in your Fastfile using the app_store_connect_api_key action with environment variables for the key ID, issuer ID, and key file path.

Can I update App Store metadata without uploading a new build?

Yes. Fastlane's deliver action lets you update release notes, descriptions, keywords, and screenshots independently of your binary. Use skip_binary_upload: true to push metadata changes without a new build.

How do I manage localized release notes with Fastlane?

Create a metadata folder with subfolders for each locale like en-US, fr-FR, and de-DE. Each subfolder contains a release_notes.txt file. Fastlane reads these automatically when you use the deliver action with a metadata_path parameter.

Can I use Fastlane in a CI/CD pipeline for Flutter apps?

Yes. Fastlane is designed to run headlessly in CI environments like GitHub Actions, GitLab CI, or Codemagic. Store your API key and certificates as CI secrets, and call your Fastlane lanes directly from your pipeline scripts.

Read more
You may also be interested in
Made by ApparenceKit logo
ApparenceKit is a flutter start kit | template generator tool by Apparence.io © 2026.
All rights reserved