Shipping RummyScoreKeeper to the Apple App Store taught me more about iOS code signing in one week than I'd learned in years of backend engineering. The combination of provisioning profiles, distribution certificates, entitlements, and App Store Connect can feel deliberately hostile to anyone who isn't already initiated. This guide is the article I wish I'd had โ a complete walkthrough from zero to App Store submission using Fastlane.
What You'll Need Before Starting
- An Apple Developer Program membership ($99/year โ required for App Store distribution)
- A Mac with Xcode installed (latest stable version)
- Your React Native project building successfully in Xcode
- Ruby installed (Fastlane runs on Ruby)
Step 1: Generate Your Certificate Signing Request (CSR)
A CSR proves to Apple that you own the private key associated with your certificate. Generate it on your Mac:
# Open Keychain Access โ Certificate Assistant โ Request a Certificate From a Certificate Authority # Fill in your email, select "Saved to disk", and save as CertificateSigningRequest.certSigningRequest # Alternatively, via command line: openssl genrsa -out distribution.key 2048 openssl req -new -key distribution.key -out distribution.csr \ -subj "/emailAddress=your@email.com/CN=Tarun Adari/C=US"
Keep your private key (distribution.key) secure. If you lose it, you'll need to revoke and regenerate your certificate. Store it in a secure location โ I use an encrypted disk image.
Step 2: Create Your Distribution Certificate
- Go to developer.apple.com โ Certificates, IDs & Profiles โ Certificates
- Click the + button, select Apple Distribution
- Upload your CSR file
- Download the resulting .cer file and double-click to install it in Keychain
You now have a Distribution certificate in your Keychain, linked to the private key you generated. Verify: open Keychain Access, find "Apple Distribution: Your Name" โ it should show a disclosure triangle with the private key beneath it. If there's no private key, the certificate is useless on this machine.
Step 3: Create an App ID and Provisioning Profile
An App ID uniquely identifies your app (e.g., com.tarunadari.rummyscorekeeper). A Provisioning Profile ties together your App ID, your distribution certificate, and your developer account into a signed bundle that Apple accepts.
# In developer.apple.com: # 1. Identifiers โ Register App ID โ set bundle ID to match your React Native project # 2. Profiles โ New Profile โ App Store โ select your App ID โ select your certificate # 3. Download the .mobileprovision file
Step 4: Install and Configure Fastlane
# Install Fastlane gem install fastlane # From your project root cd ios && fastlane init
Fastlane will ask a few questions and create a fastlane/ directory with an Appfile and Fastfile. Configure the Appfile first:
# fastlane/Appfile app_identifier("com.tarunadari.rummyscorekeeper") apple_id("your-apple-id@email.com") team_id("YOUR_TEAM_ID") # Found in developer.apple.com โ Membership
Step 5: Write the Fastfile
This is where the magic happens. The Fastfile defines your distribution lane โ the exact sequence of steps to build and submit your app:
# fastlane/Fastfile default_platform(:ios) platform :ios do lane :release do # Ensure the version and build number are set increment_build_number(xcodeproj: "RummyScoreKeeper.xcodeproj") # Build the app with manual signing build_app( workspace: "RummyScoreKeeper.xcworkspace", scheme: "RummyScoreKeeper", configuration: "Release", export_method: "app-store", export_options: { provisioningProfiles: { "com.tarunadari.rummyscorekeeper" => "RummyScoreKeeper AppStore" } } ) # Upload to App Store Connect upload_to_app_store( skip_metadata: true, skip_screenshots: true, submit_for_review: false # Set true when ready to submit ) end end
Common Gotchas
- "No provisioning profile" error: The profile name in export_options must exactly match what's in Xcode's signing settings, including spacing.
- Keychain not found in CI: If running on a CI machine, you need to create a temporary keychain and import your certificate before building. Fastlane's setup_ci action handles this.
- Build number must increment: App Store Connect rejects uploads with the same build number. Automate this with increment_build_number or you'll manually forget.
- React Native metro cache: Always clear the metro cache (npx react-native start --reset-cache) before a release build. Stale cache produces mysterious JS bundle errors.
Key Takeaways
- The CSR private key and your Distribution certificate are a matched pair โ if you lose the key, you must revoke and regenerate the certificate
- The provisioning profile name in your Fastfile export options must exactly match what Xcode sees โ case-sensitive, space-sensitive
- Always automate build number increments โ manual incrementing will cause a failed upload at the worst time
- Clear React Native metro cache before every release build to avoid stale bundle issues
- Use submit_for_review: false initially so you can review the TestFlight build before submitting to review