Target for iOS is Tauri v2 mobile. This guide covers simulator-first development with a free Apple ID; paid Developer Program work (TestFlight, ad-hoc sideload) is at the end.
| Tool | Version | Notes |
|---|---|---|
| macOS | 13+ | iOS development is Mac-only |
| Xcode | 15+ | Install from App Store or developer.apple.com — not just Command Line Tools |
| Rust | stable | rustup target add aarch64-apple-ios-sim x86_64-apple-ios (device target aarch64-apple-ios only needed for signed builds) |
| Node.js | 20+ | |
| pnpm | 9+ | corepack enable && corepack prepare pnpm@latest --activate |
| CocoaPods | 1.15+ | brew install cocoapods — required by tauri ios init to run pod install |
| Apple ID | any | Free account is sufficient for simulator + 7-day personal-device sideload |
# Install Xcode from the App Store, then point the toolchain at it:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
# Accept the license agreement once:
sudo xcodebuild -license accept
# Verify a simulator runtime is installed:
xcrun simctl list runtimesIf no iOS runtime is listed, open Xcode → Settings → Platforms and download one.
rustup target add aarch64-apple-ios-sim x86_64-apple-iosAdd aarch64-apple-ios as well only when you intend to produce signed device builds.
Tauri's ios init command runs pod install internally to set up the Xcode workspace. If pod is not on your PATH, the command will fail with a No such file or directory error.
brew install cocoapods
pod --version # verify installationsrc-tauri/gen/ is git-ignored, so the iOS Xcode project is generated on demand:
pnpm tauri ios initThis creates src-tauri/gen/apple/ containing the Xcode workspace, Info.plist, and Swift sources. Re-run this after pulling changes that alter tauri.conf.json's bundle.iOS section or icon set.
The first run prompts for a Development Team ID. For simulator-only work any value works (or leave developmentTeam: null in tauri.conf.json). For device/TestFlight work, use your team's 10-character ID from appleid.apple.com → Membership.
pnpm tauri:ios:devThe wrapper script (scripts/ios-dev.sh) verifies Xcode is selected, boots an iPhone simulator if none is running, then hands off to tauri ios dev. The Vite dev server on localhost:5174 is reachable from the simulator without any tunnelling (unlike Android's adb reverse).
pnpm tauri:ios:buildDefaults to your host arch's simulator target (aarch64-sim on Apple Silicon, x86_64-sim on Intel). The resulting .app bundle lives under src-tauri/gen/apple/build/<target>/ — drag it onto a running simulator window to install.
Pass through extra flags when you need a different target:
pnpm tauri:ios:build -- --target x86_64-sim- Open the generated workspace:
open src-tauri/gen/apple/forwardemail_mail.xcworkspace - Select the top-level project → Signing & Capabilities → sign in with your Apple ID and pick your Personal Team.
- Plug in the device, select it as the run target, press ⌘R.
Apps signed with a free team expire after 7 days and allow at most 3 apps per device. For longer-lived builds you need a paid Developer Program account — see the signed-build section below.
iOS WKWebView supports env(safe-area-inset-*) natively; src/styles/base.css:12-21 already consumes them. No Swift injection is needed (unlike MainActivity.kt on Android, which had to bridge WindowInsets manually). Verify in simulator with Hardware → Device → iPhone 15 Pro (has Dynamic Island) and rotating to landscape.
The Android build receives ACTION_SEND intents in MainActivity.kt:79-115 and dispatches app:share-received to the webview. The iOS equivalent is a Share Extension — a separate embedded target in the Xcode project that Tauri does not scaffold. Track this as follow-up work; the app functions without it.
Only required for:
- Distributing to others' devices (ad-hoc via Diawi/AltStore, or TestFlight internal/external testing)
- CI-produced signed
.ipaartifacts - Builds that survive past 7 days on your own device
Requires a paid Apple Developer Program membership ($99/yr).
- In developer.apple.com → Certificates, Identifiers & Profiles:
- Create an App ID matching
net.forwardemail.mail. - Generate an iOS Distribution certificate and download the
.cer; double-click to add to Keychain. - Create an Ad Hoc provisioning profile (for sideload) or App Store profile (for TestFlight) referencing that App ID and cert.
- Create an App ID matching
- Set
developmentTeaminsrc-tauri/tauri.conf.jsonto your 10-char team ID. - Build:
pnpm tauri ios build --target aarch64
- The resulting
.ipais atsrc-tauri/gen/apple/build/arm64/<scheme>.ipa.
The iOS job in release-mobile.yml produces a signed IPA, uploads it to App Store Connect for TestFlight distribution, and also attaches the IPA to the GitHub Release for archival. It runs on every v* tag as part of the unified release. The workflow now runs on macos-26 and explicitly selects the latest stable Xcode toolchain before building so the active iPhoneOS SDK satisfies Apple’s current submission requirement. When required secrets are missing the job logs a warning and exits 0 — it never blocks the desktop/Android release.
- Certificates → + → Apple Distribution (the unified modern cert — works for both iOS App Store and macOS App Store). Upload a CSR you generate in Keychain Access (Certificate Assistant → Request a Certificate from a Certificate Authority → Saved to disk). Download
.cer, double-click to install, then export from Keychain as a password-protected.p12. - Identifiers → verify App ID
net.forwardemail.mailexists. - Profiles → + → Distribution → App Store → pick the App ID and the Apple Distribution cert → download the
.mobileprovision.
- My Apps → + → New App → iOS, bundle ID
net.forwardemail.mail, pick a SKU likeforwardemail-mail-ios. Minimum metadata is fine to start — full screenshots/description only required before public App Store submission. - Users and Access → Integrations → App Store Connect API → Generate API Key with App Manager role. Download the
.p8(downloadable only once). Note the Key ID (10 chars) and Issuer ID (UUID at the top of the page).
Store the six iOS and App Store Connect secrets in the release environment — see SECRETS.md for the full list, Windows/macOS/Android values, and exact encoding commands.
Triggered by pnpm release (or manual v* tag push):
create-releasecuts a draft GitHub Release.build-mobilecallsrelease-mobile.ymlwhich runsandroidandiosas sibling jobs.- The iOS job:
- Imports the Apple Distribution cert into a temporary keychain.
- Installs the
.mobileprovisioninto~/Library/MobileDevice/Provisioning Profiles/by its embedded UUID. - Runs
tauri ios init --cito regeneratesrc-tauri/gen/apple/. - Runs
scripts/sync-version.cjs+scripts/inject-ios-signing.cjs, which injectCODE_SIGN_STYLE=Manual+ team + identity + profile specifier intoproject.yml, regenerate the xcodeproj viaxcodegen, writeExportOptions.plist, and setCFBundleVersionto a monotonically-increasing integer derived from semver (major*10000 + minor*100 + patch). - Runs
tauri ios build --ci --target aarch64 --export-method app-store-connectwhich archives and exports the signed IPA. - Uploads the IPA to App Store Connect via
xcrun altool --upload-appusing the API key. - Uploads the IPA to the draft GitHub Release as archival.
publishflips the Release from draft to published.
Find the build at appstoreconnect.apple.com → My Apps → Forward Email → TestFlight tab.
| Stage | Location | Typical duration | What to do |
|---|---|---|---|
| Upload received | Activity → iOS Builds → "Processing" | 5–15 min | Wait. You'll get an email when processing finishes (or fails). |
| Processing complete | TestFlight → iOS tab, listed under version | — | Build is ready for internal testers immediately. |
| Missing Compliance | Yellow banner on the build row | one-time per version | Click the build → answer the encryption question (see below). |
| Internal testing | TestFlight → Internal Testing → Groups | instant after adding | Add up to 25 testers (must be users on your team). They get an email + TF push. |
| External testing | TestFlight → External Testing → Groups | <24h beta review | Add testers by email (no Apple Developer seat needed). First build per version is beta-reviewed. |
| Build expiry | — | 90 days from upload | Upload a new build before expiry to keep testing continuous. |
Testers install the TestFlight app from the App Store once, tap the invite link or redeem the code you send them, then install. Updates are automatic.
Forward Email uses TLS for network transport and user-held PGP keys for email content — both typically qualify as exempt encryption under US EAR (§740.17(b)(1) / 5D992.c).
Two ways to handle the compliance question:
- Answer once in ASC: when the first build hits "Missing Compliance", click it → "Yes, my app uses encryption" → "Yes, the app qualifies for the exemptions" → submit. ASC remembers for future builds in the same major version.
- Set once in Info.plist to skip the prompt permanently: add
<key>ITSAppUsesNonExemptEncryption</key><false/>tosrc-tauri/gen/apple/forwardemail-desktop_iOS/Info.plist. (Not currently injected by our CI — would need to add toinject-ios-signing.cjs.)
Consult a lawyer if the app later ships non-standard crypto (custom ciphers, ECC on-device key gen without TLS context, etc).
From TestFlight → Internal Testing → + or External Testing → +:
- Internal Testing — up to 25 team members who have an App Store Connect role. Builds appear instantly after processing, no beta review. Best for developers + QA.
- External Testing — up to 10,000 testers by email (no Apple Developer Program seat needed on their end). First build per version number goes through a same-day beta review. Subsequent builds with the same major version auto-skip review.
Each tester receives an email invite with a public TestFlight link. They can also be added to a Public Link that you share anywhere — useful for "apply to beta" pages.
For rare cases where you want to produce a signed IPA outside CI (e.g., debugging a signing issue):
- Follow the one-time setup above so the Apple Distribution cert is in your login Keychain and the
.mobileprovisionis installed in~/Library/MobileDevice/Provisioning Profiles/. - Set
developmentTeaminsrc-tauri/tauri.conf.jsonto your 10-char team ID (already set). - Build:
APPLE_TEAM_ID=FH83QMJS7P \ IOS_EXPORT_METHOD=app-store-connect \ IOS_PROFILE_NAME="Forward Email Mail App Store" \ node scripts/inject-ios-signing.cjs pnpm tauri ios build --target aarch64 --export-method app-store-connect - Find the IPA at
src-tauri/gen/apple/build/arm64/*.ipa. - Upload to TestFlight via Transporter.app (Mac App Store) or
xcrun altool --upload-app.
| Symptom | Fix |
|---|---|
Failed to install cocoapods: No such file or directory |
Install CocoaPods before running tauri ios init: brew install cocoapods |
xcrun: error: unable to find utility |
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer |
No iOS Simulator runtime available |
Xcode → Settings → Platforms → install iOS SDK |
developmentTeam is required during ios init |
Set app.iOS.developmentTeam in src-tauri/tauri.conf.json, even a dummy value for simulator builds |
| Build succeeds but app doesn't launch in simulator | xcrun simctl shutdown all && xcrun simctl erase all then retry |
Rust link errors about aarch64-apple-ios missing |
rustup target add aarch64-apple-ios-sim (sim target for simulator builds) |
pnpm tauri ios dev hangs on "Waiting for connection" |
Ensure simulator is booted first (open -a Simulator) before running the command |
| White-screen in simulator | localhost:5174 not yet ready; wait for Vite to finish booting, then reload with ⌘R in Safari Web Inspector (Develop → Simulator → forwardemail_mail) |
docs/DEVELOPMENT.md— overall dev workflowdocs/desktop-setup.md— macOS desktop (Tauri) setup, some steps overlapdocs/desktop-ci-secrets.md— CI secret-management pattern to mirror for iOS signingdocs/RELEASES.md— release pipeline overview