diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..245031d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/.env
+ios/build/
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/
+fastlane/test_output/
diff --git a/fastlane/Appfile b/fastlane/Appfile
new file mode 100644
index 0000000..f2ef0b2
--- /dev/null
+++ b/fastlane/Appfile
@@ -0,0 +1,6 @@
+apple_id("james.magahern@mac.com")
+app_identifier("net.buzzert.QueueCube")
+team_id("DQQH5H6GBD")
+team_name("James Magahern")
+itc_team_id("127764897")
+itc_team_name("James Magahern")
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
new file mode 100644
index 0000000..9cd5105
--- /dev/null
+++ b/fastlane/Fastfile
@@ -0,0 +1,138 @@
+require "fileutils"
+require "open3"
+require "shellwords"
+require "tempfile"
+
+default_platform(:ios)
+
+BUNDLE_IDENTIFIER = "net.buzzert.QueueCube"
+DEVELOPMENT_TEAM = "DQQH5H6GBD"
+PROVISIONING_PROFILE_NAME = "#{BUNDLE_IDENTIFIER} AppStore"
+APP_ROOT = File.expand_path("..", File.expand_path(__dir__))
+IOS_PROJECT_DIR = File.join(APP_ROOT, "ios")
+XCODE_PROJECT = File.join(IOS_PROJECT_DIR, "QueueCube.xcodeproj")
+SCHEME = "QueueCube"
+ARCHIVE_PATH = File.join(IOS_PROJECT_DIR, "build", "#{SCHEME}.xcarchive")
+EXPORT_PATH = File.join(IOS_PROJECT_DIR, "build", "upload")
+
+def shell_command(*parts)
+ parts.flatten.map { |part| part.to_s.shellescape }.join(" ")
+end
+
+def archive_path
+ ARCHIVE_PATH
+end
+
+def export_path
+ EXPORT_PATH
+end
+
+def git_output(*args)
+ stdout, stderr, status = Open3.capture3("git", *args, chdir: APP_ROOT)
+ UI.user_error!("git #{args.join(' ')} failed: #{stderr.strip}") unless status.success?
+
+ stdout.strip
+end
+
+def app_version
+ tag = git_output("describe", "--tags", "--abbrev=0")
+ version = tag.sub(/\Av/, "")
+ unless version.match?(/\A\d+(?:\.\d+){0,2}\z/)
+ UI.user_error!("Latest git tag #{tag.inspect} is not a valid App Store version. Use a tag like 1.5.2 or v1.5.2.")
+ end
+
+ version
+end
+
+def build_number
+ git_output("rev-list", "--count", "HEAD")
+end
+
+def upload_export_options
+ <<~PLIST
+
+
+
+
+ method
+ app-store-connect
+ signingStyle
+ manual
+ teamID
+ #{DEVELOPMENT_TEAM}
+ signingCertificate
+ Apple Distribution
+ provisioningProfiles
+
+ #{BUNDLE_IDENTIFIER}
+ #{PROVISIONING_PROFILE_NAME}
+
+ manageAppVersionAndBuildNumber
+
+ stripSwiftSymbols
+
+ uploadSymbols
+
+
+
+ PLIST
+end
+
+platform :ios do
+ desc "Build QueueCube for iOS and upload the archive to TestFlight"
+ lane :beta do
+ version = app_version
+ build = build_number
+ UI.message("Using QueueCube version #{version} (build #{build}) from git")
+
+ FileUtils.rm_rf(archive_path)
+ FileUtils.rm_rf(export_path)
+
+ Dir.chdir(IOS_PROJECT_DIR) do
+ sh(shell_command(
+ "xcodebuild",
+ "-project", XCODE_PROJECT,
+ "-scheme", SCHEME,
+ "-configuration", "Release",
+ "-destination", "generic/platform=iOS",
+ "-archivePath", archive_path,
+ "-allowProvisioningUpdates",
+ "clean",
+ "archive",
+ "DEVELOPMENT_TEAM=#{DEVELOPMENT_TEAM}",
+ "PRODUCT_BUNDLE_IDENTIFIER=#{BUNDLE_IDENTIFIER}",
+ "MARKETING_VERSION=#{version}",
+ "CURRENT_PROJECT_VERSION=#{build}",
+ "CODE_SIGN_IDENTITY=Apple Distribution"
+ ))
+ end
+
+ export_options = Tempfile.new(["queuecube-export-options", ".plist"])
+ export_options.write(upload_export_options)
+ export_options.close
+
+ FileUtils.rm_rf(export_path)
+ Dir.chdir(IOS_PROJECT_DIR) do
+ sh(shell_command(
+ "xcodebuild",
+ "-exportArchive",
+ "-archivePath", archive_path,
+ "-exportPath", export_path,
+ "-exportOptionsPlist", export_options.path,
+ "-allowProvisioningUpdates"
+ ))
+ end
+
+ ipa_path = Dir[File.join(export_path, "*.ipa")].first
+ UI.user_error!("No IPA found in #{export_path}") unless ipa_path
+
+ upload_to_testflight(
+ app_identifier: BUNDLE_IDENTIFIER,
+ ipa: ipa_path,
+ skip_waiting_for_build_processing: true,
+ uses_non_exempt_encryption: false
+ )
+ ensure
+ export_options&.unlink
+ end
+end
diff --git a/fastlane/README.md b/fastlane/README.md
new file mode 100644
index 0000000..2bde69e
--- /dev/null
+++ b/fastlane/README.md
@@ -0,0 +1,41 @@
+fastlane documentation
+----
+
+# Installation
+
+Make sure you have the latest version of the Xcode command line tools installed:
+
+```sh
+xcode-select --install
+```
+
+For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
+
+# Available Actions
+
+## iOS
+
+### ios beta
+
+```sh
+[bundle exec] fastlane ios beta
+```
+
+Build QueueCube for iOS and upload the archive to TestFlight
+
+The lane uses the latest reachable git tag as `MARKETING_VERSION` and the git commit count as `CURRENT_PROJECT_VERSION`.
+
+Set `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD` in your shell before uploading.
+
+```sh
+export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=""
+fastlane ios beta
+```
+
+----
+
+This README.md follows the format generated by [_fastlane_](https://fastlane.tools).
+
+More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
+
+The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).