Ship fastlane builds to testers without TestFlight

Your Fastfile already builds and signs the app. Distribution is one more line: call the BetaDrop CLI from the lane with fastlane's sh action, and it returns an over-the-air install link. Testers open the link on their phone, scan the QR code, and install from the browser — no TestFlight invite, no review wait, no tester accounts.

There is no plugin to add to your Pluginfile and nothing to configure beyond a token. The lanes below are complete and copy-pasteable: one for an iOS ad-hoc .ipa, one for an Android release .apk, plus the step that posts the resulting link to Slack.

One-time setup: the CLI and a token

The CLI is a free npm package, installed once on whatever machine runs fastlane. For authentication, create a token on betadrop.app under Settings → Developer → API tokens and expose it as BETADROP_TOKEN. On CI, store it as a secret; on your own Mac, betadrop login once does the same job interactively.

terminal
# Once, on the machine that runs fastlane (your Mac or the CI runner)
npm install -g @betadrop/cli

# CI: store a token as a secret and expose it as BETADROP_TOKEN —
# betadrop publish reads it from the environment automatically.
export BETADROP_TOKEN=bd_live_xxxxxxxxxxxxxxxxxxxx

That is the whole footprint. If your pipeline runs on GitHub Actions rather than a Fastfile, the same two commands work as a workflow step — see the GitHub Actions integration for the YAML version.

The iOS lane: gym, then publish

gym archives and exports the app; export_method: "ad-hoc" is what makes the build installable outside the App Store. After it runs, gym leaves the .ipa's absolute path in lane_context[SharedValues::IPA_OUTPUT_PATH], so the publish step never hardcodes an output directory. Because sh returns the command's stdout, the --ci flag — which prints only the install URL — turns the CLI call into an assignment.

fastlane/Fastfile
platform :ios do
  desc "Build an ad-hoc IPA and publish it to BetaDrop"
  lane :beta do
    gym(
      scheme: "MyApp",
      export_method: "ad-hoc"
    )

    # gym drops the built IPA's absolute path into the shared lane context.
    # Quote the interpolation — a scheme like "My App" exports a path with a space
    # in it, and unquoted that reaches the CLI as two arguments.
    # --ci makes the CLI print only the install URL, so sh() can capture it.
    install_url = sh("betadrop publish \"#{lane_context[SharedValues::IPA_OUTPUT_PATH]}\" --ci").strip

    UI.success("Testers install from: #{install_url}")
  end
end

BetaDrop generates the itms-services manifest automatically, so the link installs straight from Safari. The usual Apple rules still apply — an ad-hoc build installs only on devices whose UDIDs are in the provisioning profile, which is Apple's signing model, not something any distribution service can waive. Collect UDIDs with the UDID checker before you regenerate the profile. The --name and --notes flags exist if you want the build labeled with a version or changelog when testers open the install page.

The Android lane: gradle, then publish

Same shape, different builder. Use assembleRelease, not bundleRelease: BetaDrop distributes installable .apk files and does not accept .aab bundles or re-sign builds. The .aab is for the Play Store; the APK is what a tester's phone can actually sideload.

fastlane/Fastfile
platform :android do
  desc "Build a release APK and publish it to BetaDrop"
  lane :beta do
    gradle(task: "assembleRelease")

    # Quoted for the same reason as the iOS lane: a checkout or module path with a
    # space in it would otherwise reach the CLI as two arguments.
    install_url = sh("betadrop publish \"#{lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]}\" --ci").strip

    UI.success("Testers install from: #{install_url}")
  end
end

gradle records the built APK's path in lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], so this lane survives module renames and output-directory changes untouched. Android has no UDID gate: anyone with the link can install, as long as the build is signed with your release (or debug) config. Builds up to 500 MB publish on the free tier, and the same upload works from the browser when you want to sanity-check a one-off outside the pipeline.

Put the link where testers will see it

A build nobody can find is a build nobody tests. Since install_url is just a lane variable, hand it to fastlane's built-in slack action and every green build announces itself:

fastlane/Fastfile
# After the publish step in either lane:
slack(
  slack_url: ENV["SLACK_WEBHOOK_URL"],
  message: "New beta ready — install from your phone: #{install_url}",
  success: true
)

No Slack? The UI.success line in the lanes above already prints the URL into the build log, and the install page itself shows a QR code — useful when the tester is sitting next to you and the fastest transfer is pointing a camera at a screen.

Trigger the lane however you already trigger builds: fastlane ios beta from a nightly job, on every merge to main, or by hand when a client asks for the latest. Each publish creates a fresh link, so testers always install the newest build instead of digging through channel history for an old one. Links expire after 3 days by default — long enough for a review cycle, short enough that a stale build cannot keep circulating — and free accounts can extend a build's retention up to 30 days.

Signing: point match at it once

If certificates and profiles are still copied between laptops by hand, fastlane's match is the fix: it keeps your signing identity in encrypted shared storage and syncs it to any machine — including the CI runner — so gym always finds a valid ad-hoc profile. Run match(type: "adhoc", force_for_new_devices: true) before gym in the lane. That flag is what makes match notice newly registered devices and regenerate the profile — without it match reuses the profile it already has, and a tester you added this morning still cannot install. Everything happens on machines you control, which is exactly where signing belongs — our IPA signing explainer covers why you should never hand a private key to a website, ours included.

When pilot and TestFlight are the better choice

fastlane's pilot uploads to TestFlight, and there are real cases where that is the right lane to write instead. If the build is headed to the App Store anyway, the first build you push to an external TestFlight group goes to Apple's beta review, and subsequent builds may not require a full review — you pay that gate once per group either way, so you may as well collect TestFlight's managed updates and feedback along the way. And if you need thousands of external testers, TestFlight's 10,000-tester capacity beats ad-hoc distribution outright, because ad-hoc is capped at 100 registered devices per product family per membership year on a standard Apple developer account (Apple, checked August 2026).

BetaDrop wins the other cases: daily builds a handful of testers should install within minutes, clients who will never enroll as testers in App Store Connect, Android and iOS from the same Fastfile with the same one-line step, and any team that has lost an afternoon to a stuck beta review. The TestFlight comparison walks through the trade-off in full.

One more thing said plainly: there is no official BetaDrop fastlane plugin yet. The sh call above is not a workaround pending one — it is the supported path, and at one line it is unlikely a plugin would ever make your Fastfile shorter.

Try it before you automate it

The fastest way to trust the pipeline is to see one build travel the whole path. Drag your latest .ipa or .apk into the uploader, open the link on a phone, and then let the lane do it from here on.

Frequently asked questions

Is there an official BetaDrop fastlane plugin?

Not yet. The supported path is calling the BetaDrop CLI from a lane with fastlane's sh action — install it once with npm install -g @betadrop/cli and the publish step is a single line. A plugin would wrap that same call without making it any shorter, so the CLI is not a stopgap; it is the intended integration.

How does betadrop publish authenticate on a CI machine?

Set BETADROP_TOKEN in the environment. Create a token under Settings → Developer → API tokens on betadrop.app, store it as a secret in your CI provider, and betadrop publish picks it up automatically — no login step inside the lane. On your own Mac you can run betadrop login once instead.

Can fastlane distribute an iOS build without TestFlight?

Yes. Export with gym using export_method 'ad-hoc', then publish the .ipa to BetaDrop. BetaDrop generates the itms-services manifest, so testers install straight from Safari with no TestFlight invite and no review wait. Apple's signing rules still apply: an ad-hoc build installs only on devices whose UDIDs are in the provisioning profile.

Does this work with an Android App Bundle (.aab)?

No. BetaDrop distributes installable .apk files and does not re-sign or convert bundles, so build with gradle(task: 'assembleRelease') rather than bundleRelease. Keep the .aab for your Play Store upload; the APK is what testers actually install.

What does the --ci flag do?

It makes betadrop publish print only the install URL on stdout. That is what lets a lane capture the link into a variable with sh(...) and hand it to Slack, a pull-request comment, or the build log, instead of parsing decorated human-readable output.

iMobile Designs
Developed by iMobile Designs
Made with
in India