Beta Testing .NET MAUI 10: TestFlight vs Play Internal vs Firebase (2026)

Compare TestFlight, Google Play Internal Testing, and Firebase App Distribution for .NET MAUI 10 apps. Fastlane commands, a GitHub Actions matrix, and a per-layer channel picker for engineering, QA, and pre-launch beta.

Updated: August 28, 2026

For .NET MAUI 10 apps, the three practical beta distribution channels in 2026 are Apple TestFlight (iOS-only, up to 10,000 external testers, 90-day build expiry), Google Play Internal Testing (Android-only, up to 100 internal testers with instant availability), and Firebase App Distribution (cross-platform, unlimited testers, no store review). Honestly, most teams end up using all three: TestFlight and Play Internal for pre-store validation, Firebase App Distribution for daily developer builds and ad hoc external QA. The right pick depends on whether you need store review parity, cross-platform tester lists, or fast turnaround for engineers.

  • TestFlight is mandatory for external iOS beta testing but requires Apple review for each new build sent to external groups and expires after 90 days.
  • Google Play Internal Testing publishes App Bundles to up to 100 testers in minutes with no review, while Closed and Open tracks add review and higher tester caps.
  • Firebase App Distribution accepts IPA and APK/AAB uploads, has no per-build review, and works across iOS and Android with a single tester list.
  • For .NET MAUI 10, all three integrate cleanly with Fastlane (pilot, supply, firebase_app_distribution) and can be wired into GitHub Actions or Azure DevOps.
  • iOS builds distributed via Firebase App Distribution need ad hoc provisioning and each tester device UDID registered in the Apple Developer portal.
  • A pragmatic 2026 setup: Firebase for internal engineering dailies, Play Internal Testing for Android QA, TestFlight for external iOS beta and pre-release validation.

Why most .NET MAUI teams end up using all three

Most teams skip this part, and then two weeks before launch someone realises the external TestFlight group needs a fresh Apple review because the last build expired. I've watched this exact fire drill twice, and it always eats the same three days. Beta distribution isn't one channel. It's a layered pipeline. The bottom layer is engineering-only: builds fired off every commit for developers and internal QA. The middle layer is closed testing, real testers on real devices without the friction of store review. The top layer is external pre-release: hundreds or thousands of end users receiving builds that closely mirror what will ship.

Each of the three services covers a different layer well. Firebase App Distribution is the fastest for the bottom layer: no review, IPA/AAB uploaded straight from CI, testers get an email link in seconds. Google Play Internal Testing is the sweet spot for Android closed testing because it validates the exact App Bundle you'll promote to production, and there's no review delay. TestFlight is where external iOS beta lives because Apple simply doesn't allow ad hoc distribution beyond 100 devices, and any wider group must go through TestFlight.

The instinct to pick "one channel to rule them all" fails on cross-platform reality. Apple's rules make Firebase's cross-platform promise partial (iOS still needs UDIDs registered), and Google's rules make TestFlight non-viable on Android. Accepting that up front saves a week of arguing.

TestFlight vs Google Play Internal vs Firebase App Distribution

The dimensions that actually matter for a .NET MAUI 10 team are platform coverage, tester limits, review latency, artifact format, CI story, and cost. Here's the side-by-side.

DimensionTestFlightGoogle Play Internal TestingFirebase App Distribution
PlatformiOS onlyAndroid onlyiOS and Android
Max testers10,000 external + 100 internal100 internal (Closed and Open tracks are larger)Unlimited
Review requiredYes for external groups; no for internal Apple teamNoNo
Build availabilityMinutes for internal; hours to days for external (Beta App Review)Typically minutesTypically minutes
ArtifactIPA uploaded to App Store ConnectApp Bundle (.aab) or APKIPA (ad hoc) or APK/AAB
Build expiry90 daysNone (until removed)150 days by default (configurable)
ProvisioningStore distribution profilePlay upload signingiOS needs ad hoc profile with tester UDIDs
CostIncluded with Apple Developer Program ($99/yr)Included with Play Console ($25 one-time)Free tier is generous; billed via Firebase project
Fastlane toolpilotsupply (with --track internal)firebase_app_distribution

TestFlight for .NET MAUI 10: setup and gotchas

TestFlight is Apple's official beta channel and, in practice, the only realistic way to distribute an iOS beta to more than 100 devices. For .NET MAUI 10 (which requires Xcode 16.2+ and .NET 10 SDK), the workflow is: build a release IPA signed with an App Store distribution certificate, upload it to App Store Connect, then invite testers via TestFlight.

Build an App Store IPA from .NET MAUI 10

dotnet publish -f net10.0-ios \
  -c Release \
  -p:ArchiveOnBuild=true \
  -p:CodesignProvision="MyApp AppStore" \
  -p:CodesignKey="Apple Distribution: My Company (TEAM123)" \
  -p:RuntimeIdentifier=ios-arm64

The output is an .ipa under bin/Release/net10.0-ios/ios-arm64/publish/. Upload it with xcrun altool (deprecated but still works in 2026 for legacy CI) or the newer xcrun notarytool-free flow through xcrun ttp upload. Fastlane's pilot upload is what most teams use in practice.

The 90-day expiry that catches teams

Every TestFlight build expires 90 days after upload. Testers see "This beta has expired" and can no longer launch the app. So if your release cadence is longer than 90 days (common for internal tools or slow-moving apps), schedule a rebuild-and-reupload job. The Apple TestFlight documentation confirms this window has not changed for 2026.

App-Specific Passwords and API keys

For CI uploads, stop using app-specific passwords. In 2026 Apple pushes teams to App Store Connect API keys. Generate a key with "App Manager" role, store the .p8 file, key ID, and issuer ID as CI secrets, and pass them to pilot via --api_key_path. This avoids 2FA prompts and works with modern Xcode toolchains.

Google Play Internal Testing for .NET MAUI 10

Google Play has four testing tracks: Internal, Closed, Open, and Production. Internal Testing is the fastest of the four. Publish an AAB and up to 100 testers on your list get access in minutes with no review. Closed Testing adds a required review and higher tester caps (via email lists or Google Groups). Open Testing makes the app discoverable by anyone with the opt-in URL.

Build an AAB and push it to the Internal track

dotnet publish -f net10.0-android \
  -c Release \
  -p:AndroidPackageFormat=aab \
  -p:AndroidKeyStore=true \
  -p:AndroidSigningKeyStore=$KEYSTORE_PATH \
  -p:AndroidSigningStorePass=$KEYSTORE_PASS \
  -p:AndroidSigningKeyAlias=$KEY_ALIAS \
  -p:AndroidSigningKeyPass=$KEY_PASS

The AAB lands under bin/Release/net10.0-android/publish/. Upload it via Fastlane's supply:

bundle exec fastlane supply \
  --package_name com.mycompany.myapp \
  --aab bin/Release/net10.0-android/publish/com.mycompany.myapp-Signed.aab \
  --track internal \
  --json_key play-service-account.json \
  --release_status draft

Set --release_status completed if you want the build to become available immediately; draft lets you review the rollout percentage in Play Console before promoting.

Version code strategy

Google Play rejects an upload if versionCode hasn't increased. For .NET MAUI 10, set <ApplicationVersion> in the .csproj to a number that CI increments per build. A common pattern is a Unix-timestamp-based versionCode for beta builds and a semantic version code (major*1000 + minor*100 + patch) for production. Mixing them causes upload failures, so pick one and document it. Our related guide on code signing .NET MAUI apps in CI/CD covers the keystore-secret handling for the same pipeline.

Testers see the app in the Play Store, not sideloaded

The key advantage of Play Internal Testing over ad hoc distribution: testers install the app through the actual Play Store, so update flows, Play services, in-app updates, and licensing all behave exactly like production. If you're testing an in-app update flow, this is the only realistic way to verify it.

Firebase App Distribution for .NET MAUI 10

Firebase App Distribution is Google's cross-platform beta channel. It accepts IPA files for iOS and APK/AAB files for Android, doesn't review builds, and lets you organise testers into named groups. Testers install via a small helper app (iOS) or direct APK sideload (Android). For internal engineering dailies, it's the fastest option of the three.

Upload from CLI

# Android APK to a tester group
firebase appdistribution:distribute \
  bin/Release/net10.0-android/com.mycompany.myapp-Signed.apk \
  --app 1:1234567890:android:abcdef \
  --release-notes "Nightly build $(git rev-parse --short HEAD)" \
  --groups "engineering,qa"

# iOS IPA (ad hoc)
firebase appdistribution:distribute \
  bin/Release/net10.0-ios/ios-arm64/publish/MyApp.ipa \
  --app 1:1234567890:ios:abcdef \
  --release-notes "Nightly build $(git rev-parse --short HEAD)" \
  --groups "engineering,qa"

The iOS ad hoc gotcha

Firebase App Distribution does not bypass Apple's provisioning rules. For iOS builds, you need an ad hoc provisioning profile that lists every tester device UDID. Firebase does help here: it can auto-collect UDIDs when a new tester registers, then export them so you can add them to the Apple Developer portal and rebuild. That "rebuild after adding UDID" loop is the single most common frustration for teams new to Firebase on iOS. I hit this exact loop on my first ad hoc rollout, and the fastest fix is scripting the UDID import as part of the same nightly job that rebuilds the IPA. The Firebase App Distribution documentation for iOS walks through the flow.

Crash reports and analytics link up

The reason many teams pick Firebase App Distribution for engineering builds is the tight coupling with Crashlytics and Analytics. A crash on a beta build lands in the same dashboard as a crash on production, and you can filter by release. If you've replaced App Center with a modern stack (see crash reporting in .NET MAUI 10 after App Center), Firebase App Distribution is the natural distribution side of that story.

Automating beta releases from CI/CD

Every one of these channels has a Fastlane action, and every one is a one-liner from a GitHub Actions job. The right shape is a "beta" workflow that runs on push to a release/* branch and fans out to the three targets in parallel. Our deeper walkthrough on setting up CI/CD for .NET MAUI with GitHub Actions and Azure DevOps covers the runner-selection and secret-management side; here's the distribution layer.

GitHub Actions matrix example

name: beta
on:
  push:
    branches: [release/*]
jobs:
  ios-testflight:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-dotnet@v5
        with: { dotnet-version: '10.0.x' }
      - run: dotnet workload install maui-ios
      - name: Build IPA
        run: dotnet publish -f net10.0-ios -c Release -p:ArchiveOnBuild=true
      - name: Upload to TestFlight
        env:
          APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.ASC_KEY_ID }}
          APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.ASC_KEY }}
        run: |
          bundle exec fastlane pilot upload \
            --ipa bin/Release/net10.0-ios/ios-arm64/publish/MyApp.ipa \
            --skip_waiting_for_build_processing true

  android-play-internal:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-dotnet@v5
        with: { dotnet-version: '10.0.x' }
      - run: dotnet workload install maui-android
      - name: Build AAB
        run: dotnet publish -f net10.0-android -c Release -p:AndroidPackageFormat=aab
      - name: Upload to Play Internal
        env:
          PLAY_JSON_KEY: ${{ secrets.PLAY_JSON_KEY }}
        run: |
          echo "$PLAY_JSON_KEY" > play.json
          bundle exec fastlane supply \
            --package_name com.mycompany.myapp \
            --aab bin/Release/net10.0-android/publish/com.mycompany.myapp-Signed.aab \
            --track internal --json_key play.json

  cross-firebase:
    needs: [ios-testflight, android-play-internal]
    runs-on: ubuntu-24.04
    steps:
      - name: Distribute nightly to Firebase
        run: |
          firebase appdistribution:distribute app.apk \
            --app ${{ secrets.FIREBASE_APP_ID }} \
            --groups engineering

Symbol upload on the same job

Whichever channel distributes the build, testers will produce crashes, and crashes without symbolicated dSYMs and R8 mapping files are worthless. Upload symbols in the same job that produced the artifact; the filesystem paths are already right, and you avoid the "which build did this crash come from?" ambiguity that hits teams that upload symbols on a separate cadence.

How do you choose between them?

The decision usually comes down to who the testers are and how quickly you need them to see a build. So here's a pragmatic default that works for most .NET MAUI 10 teams in 2026:

  1. Every commit to main: Firebase App Distribution to an "engineering" group. Fast feedback for developers, no review, crashes flow into Crashlytics.
  2. Every commit to release/*: Play Internal Testing (Android) and TestFlight internal group (iOS). Both are fast, both mirror production install flow, both are what QA will actually test on.
  3. Pre-launch external beta: TestFlight external group (iOS, needs Beta App Review) and Play Closed Testing (Android, needs review). Plan the beta cycle around review latency.
  4. Ad hoc "send this build to a specific person right now": Firebase App Distribution. Add them to a one-off group, distribute, delete the group after.

Skip Firebase App Distribution for iOS when tester lists get large, because the UDID registration overhead exceeds the value. Skip TestFlight for pure engineering dailies. The review latency for external groups and 90-day expiry aren't worth fighting for a build only three people will install.

Frequently Asked Questions

Can you use Firebase App Distribution for iOS in 2026?

Yes. Firebase App Distribution accepts iOS IPA files signed with an ad hoc provisioning profile. Each tester's device UDID must be added to the profile in the Apple Developer portal before you build, and Firebase can auto-collect UDIDs from new testers to make that loop easier. Ad hoc is capped at 100 devices per Apple membership year, so for larger external betas you still need TestFlight.

Does TestFlight cost money?

No additional charge. TestFlight is included in the Apple Developer Program membership ($99/year), and there are no per-tester or per-build fees. The only cost is the review latency for external testing groups (typically under 24 hours in 2026).

How many testers can Google Play Internal Testing support?

Up to 100 testers on the Internal track, added by email address or Google Group. If you need more, promote the release to Closed Testing (which can scale to thousands and supports Google Groups or email lists) or Open Testing (unlimited, discoverable via opt-in URL).

Why does my TestFlight build expire after 90 days?

Apple sets a hard 90-day expiration on every TestFlight build. This forces active projects to keep shipping fresh builds and prevents beta apps from lingering indefinitely. If your release cadence is slower than 90 days, schedule a rebuild-and-reupload job to prevent testers from being locked out of the app.

Can I use one Fastlane pipeline for all three channels?

Yes. Fastlane ships pilot for TestFlight, supply for Google Play (with --track internal), and firebase_app_distribution as a plugin. A single Fastfile can define a beta lane that builds the .NET MAUI 10 IPA and AAB, then fans out uploads to all three services in parallel from CI.

Sofia Rodriguez
About the Author Sofia Rodriguez

Mobile DevOps engineer focused on the unglamorous stuff: build pipelines, signing, store releases, and the tooling that keeps teams shipping.