# Ad Hoc Distribution With fastlane, Without Rebuilding for New Devices

> Add one line to your Fastfile and every build_app reaches testers’ iPhones. No register_devices, no profile refresh and no rebuild when someone new joins.

Last updated: 2026-11-05
Canonical: https://buildferry.app/fastlane-ad-hoc-distribution

A typical fastlane ad hoc lane registers new devices, refreshes the provisioning profile, rebuilds and uploads. BuildFerry takes over the first three for each tester you approve, so your lane only runs when the code changes.

## The usual ad hoc lane

```
lane :adhoc do
  register_devices(devices_file: "devices.txt")
  match(type: "adhoc", force_for_new_devices: true)
  build_app(export_method: "ad-hoc")
  # then upload the .ipa somewhere testers can reach it
end
```

Every new tester means collecting their UDID, adding it to `devices.txt` and running the whole lane again.

## The lane with BuildFerry

```
lane :testers do
  build_app(export_method: "ad-hoc")
  sh("npx", "--yes", "buildferry@1", "upload", lane_context[SharedValues::IPA_OUTPUT_PATH])
end
```

Set `BUILDFERRY_TOKEN` to a token from **Upload from CI** in the BuildFerry dashboard, in your CI's secrets or your shell. The last line uploads the .ipa that `build_app` just made, waits while BuildFerry checks it, and prints the share link. If a build can't be shared, for example an App Store export, the lane stops with the reason.

It runs the BuildFerry CLI, so the machine needs Node.js 18 or newer. GitHub's macOS runners and most CI images already have it.

## What happens when someone new joins

1. They open your app's share link in Safari and allow a one-time profile. That's how BuildFerry gets the UDID.
2. You approve their iPhone in the dashboard.
3. BuildFerry registers it with your Apple team, updates the profile and re-signs your latest build in the cloud.
4. They tap **Install**.

Your lane doesn't run, and `devices.txt` doesn't change.

## Useful options

| Option | |
| --- | --- |
| `--no-wait` | Don't wait for the check |
| `--timeout 900` | Wait longer for a very large build (seconds) |
| `--json` | Print only the result as JSON: status, version, build, shareUrl, appUrl and testers |

On GitHub Actions you can skip fastlane for this step and use [the BuildFerry action](https://buildferry.app/github-actions-ios-distribution).
