DEV Community

Cover image for Automate Your Flutter iOS Deployment with Fastlane
Gautier 💙
Gautier 💙

Posted on • Originally published at apparencekit.dev

Automate Your 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
Enter fullscreen mode Exit fullscreen mode

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

cd ios
fastlane init
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Run it with:

fastlane download_app_metadata
Enter fullscreen mode Exit fullscreen mode

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.

(I tend to split screenshots and metadata into separate lanes, so I can update text without worrying about images, and vice versa.)

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[ar-SA ca cs da de-DE el en-AU en-CA en-GB en-US
               es-ES es-MX fi fr-CA fr-FR he hi hr hu id it ja
               ko ms nl-NL no pl pt-BR pt-PT ro ru sk sv th tr
               uk vi zh-Hans zh-Hant]
  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
Enter fullscreen mode Exit fullscreen mode

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.
You can remove any locale you don't support, but keep the fallback to avoid submission failures.

Note:
You can create a translations script to automatically translate your release notes from English to all your other languages.

All Apple App Store locales by priority

Not all locales are equal. Some cover massive markets with high App Store spending. Others are nice-to-have. Here is a prioritization to help you decide what to translate first.

Tier 1 — Cover these first. These locales represent the largest App Store markets by revenue and downloads. Skipping any of them means leaving money on the table.

Locale Language Why
en-US English (US) Largest App Store market by revenue
en-GB English (UK) Second-largest English-speaking market (optionnal as it's covered by en-US)
ja Japanese Japan is the #2 App Store market worldwide
zh-Hans Chinese Simplified Massive user base, high spending on in-app purchases
ko Korean South Korea has one of the highest ARPU
de-DE German Germany is the top European market
fr-FR French (France) Major European market and 2nd language impact on surrounding countries
es-ES Spanish (Spain) Gateway to all Spanish-speaking countries
pt-BR Portuguese (Brazil) Largest Latin American market
en-AU English (Australia) High ARPU English-speaking market

Tier 2 — Add these next. Strong markets that will move the needle once Tier 1 is covered.

Locale Language Why
zh-Hant Chinese Traditional Covers Taiwan and Hong Kong
it Italian Large European economy
nl-NL Dutch High smartphone penetration
sv Swedish High ARPU Nordic market
da Danish High ARPU Nordic market
no Norwegian High ARPU Nordic market
tr Turkish Large and growing mobile market
pl Polish Biggest Central European market
ar-SA Arabic (Saudi Arabia) High spending Gulf market
he Hebrew Israel is a strong tech market
en-CA English (Canada) High ARPU North American market
fr-CA French (Canada) Required for Quebec App Store visibility
es-MX Spanish (Mexico) Second-largest Latin American market
pt-PT Portuguese (Portugal) Completes Portuguese coverage

Tier 3 — Nice to have. Smaller markets or languages where English metadata often performs just as well. Add them when you have the bandwidth.

Locale Language why
ca Catalan
cs Czech
ru Russian Large user base but currently can't pay for apps
el Greek
fi Finnish
hi Hindi
hr Croatian
hu Hungarian
id Indonesian
ms Malay
ro Romanian
sk Slovak
th Thai
uk Ukrainian
vi Vietnamese can be used to impact US market

A practical approach: start with Tier 1 using quality translations, cover Tier 2 with machine translation reviewed by a native speaker, and use English fallbacks for Tier 3 until your app gains traction in those markets.


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
Enter fullscreen mode Exit fullscreen mode

Run it:

fastlane release_new_version version:"1.2.0"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Deploy without submitting for review:

fastlane deploy
Enter fullscreen mode Exit fullscreen mode

Deploy and submit for review in one shot:

fastlane deploy submit_for_review:true
Enter fullscreen mode Exit fullscreen mode

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[ar-SA ca cs da de-DE el en-AU en-CA en-GB en-US
                 es-ES es-MX fi fr-CA fr-FR he hi hr hu id it ja
                 ko ms nl-NL no pl pt-BR pt-PT ro ru sk sv th tr
                 uk vi zh-Hans zh-Hant]
    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
Enter fullscreen mode Exit fullscreen mode

No reasons to not have metadata for all locales

One of the best reason to handle metadata with Fastlane is that you can version control your ASO.
(ASO = App Store Optimization. The art of writing good descriptions, release notes, and keywords to rank higher in search results. This is SEO for the App Store.)

When your metadata is in git, you can see the history of every change, revert if needed, and even have translators submit PRs with updated descriptions and release notes.

But don't translate litterally everything.
Some countries, even if they are non-English, will have better ASO with English metadata.

I'll write another article about ASO best practices.


My typical deploy workflow

Here is what a release looks like in practice:

# 1. Build the Flutter app
flutter build ipa

# 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
Enter fullscreen mode Exit fullscreen mode

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.

Bonus I also made a script to fetch the pubspec version and run all commands in one go.
And for Android too but this article is about iOS so I won't go into that.


What about first release?

Start by making all metadata research for US market. Write your description, release notes, and keywords in English.

Then use a script to translate description, promotional text to all other languages.
For title, subtitle, and keywords I tend to keep writing them manually as they require some research and creativity for each market.

Don't forget screenshots.
I made a plugin to automate screenshot translations using figma. If that interests you, let me know and I can write an article about it.


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.

Top comments (0)