Hosted Device Farms
Tier 4 — runs mobile tests on real devices in BrowserStack App Automate or Sauce Labs Real Device Cloud. Best for iOS coverage without owning Macs, real-device behavior (sensors, biometrics, OEM-specific bugs), and burst capacity.
The platform handles vendor-specific quirks (auth, APK upload, capability blocks) automatically. Same YAML, same Job History.
What the platform does for you
- Auto-upload the APK — agent fetches your stored APK from MinIO and uploads it to the vendor at job time. No manual
curlto/app-automate/upload. - Credentials from the vault — vendor keys live once in the environment credential vault, referenced by tag.
- Vendor-specific capability blocks —
bstack:optionsfor BrowserStack,sauce:optionsfor Sauce Labs are built automatically with the right structure. - Region-aware URLs — Sauce Labs hub URL is templated by region (
us-west-1,eu-central-1,apac-southeast-1).
Quick start: BrowserStack
1. Store credentials in the vault
In your environment's auth config, add a credential set:
{
"credentials": [{
"tag": "prod-browserstack",
"username": "your-bs-username",
"password": "your-bs-access-key",
"isDefault": false
}]
}
The password field carries the access key (semantically the secret). Encrypted at rest with AES-GCM.
You can have multiple credential sets — e.g. dev-browserstack and prod-browserstack. Set one as the default with defaultCredentialTag on the environment if you want apps to use it implicitly.
2. Configure the mobile app
In Configuration → Mobile Apps, edit your app:
| Field | Value |
|---|---|
| Cloud provider | BROWSERSTACK |
| Cloud config (JSON) | See below |
{
"credentialTag": "prod-browserstack",
"deviceName": "Google Pixel 7",
"osVersion": "13.0",
"projectName": "MyApp Tests"
}
That's it. No appUrl, no userName, no accessKey in the cloud config — the platform resolves the tag from the vault at job time.
3. Make sure your APK is uploaded
Through Mobile Apps → Upload Binary (the existing flow). The platform stores it in MinIO.
4. Run a test
Any existing mobile test against this app. The agent will:
- Download the APK from MinIO
- Upload it to
https://api-cloud.browserstack.com/app-automate/upload - Connect to
https://hub-cloud.browserstack.com/wd/hubwithbs://<uploaded-hash>as the app capability - Run the test on the requested device
Watch the agent log:
DF.1.c — using resolved cloud_config from vault (provider=BROWSERSTACK)
DF.1.b — no appUrl in cloud_config; auto-uploading APK to BrowserStack
DF.1.b — BrowserStack upload OK, app_url=bs:**...***
Creating BrowserStack driver: hub=https://hub-cloud.browserstack.com/wd/hub, device=Google Pixel 7
The test then runs on the device and reports back to Job History as usual.
5. (Optional) Pre-uploaded apps for faster startup
Per-job upload adds ~5 seconds. For high-frequency CI, upload once via the BrowserStack REST API and put the returned bs://... URL in cloud_config.appUrl:
{
"credentialTag": "prod-browserstack",
"appUrl": "bs://abc123def456",
"deviceName": "Google Pixel 7"
}
When appUrl is set, the agent skips the auto-upload step. Update the URL when the APK version changes.
Quick start: Sauce Labs
Same shape as BrowserStack. Differences:
| Aspect | BrowserStack | Sauce Labs |
|---|---|---|
| Credential mapping | username + accessKey | username + accessKey (same) |
| Upload endpoint | /app-automate/upload | /v1/storage/upload |
| App reference shape | bs://abc123... | storage:filename=app.apk |
| Hub URL | hub-cloud.browserstack.com | ondemand.{region}.saucelabs.com |
| Options block | bstack:options | sauce:options |
| Regions | one global | us-west-1 (default), eu-central-1, apac-southeast-1 |
Cloud config example
{
"credentialTag": "prod-saucelabs",
"region": "eu-central-1",
"deviceName": "Google Pixel 7",
"platformVersion": "13.0"
}
That's all. The agent will:
- Upload APK to
https://api.eu-central-1.saucelabs.com/v1/storage/upload - Connect to
https://ondemand.eu-central-1.saucelabs.com/wd/hub - Run the test on the requested device
Credential pattern: default vs choose
The platform follows the same vault precedence pattern as API scenarios:
| What you set | What happens |
|---|---|
cloud_config.credentialTag: "prod-bs" | Explicit — use that vault entry |
No credentialTag, env has a defaultCredentialTag | Default — use the env's default credential |
No credentialTag, env has a credential with isDefault: true | Default — use that one |
| Neither tag nor default exist | Agent passes through; provider validation surfaces "missing userName" |
Inline userName + accessKey set (legacy) | Pass through unchanged |
Most teams set one credential per environment + flag it as default. Then mobile apps just need { "deviceName": "..." } — no credential boilerplate.
Switching an existing test
Already running on T1 (local) and want to graduate to a real device? Only change cloud_provider:
Cloud provider: NONE → BROWSERSTACK
Cloud config: {} → {
+ "credentialTag": "prod-browserstack",
+ "deviceName": "Google Pixel 7"
+ }
The YAML doesn't change. The agent automatically:
- Downloads your APK from MinIO (already there from T1 setup)
- Uploads it to BrowserStack
- Connects to the right hub
- Runs the test
- Reports results to the same Job History
Device matrix (multiple devices)
Today: configure one device per mobile app. To run against a matrix:
- Clone the mobile app once per device (or per OS version)
- Each clone has a different
deviceName/osVersionin cloud_config - Run the same test against each — they appear as separate runs in Job History
Future: declarative matrix on a single test (devices: ["Pixel 7", "Galaxy S23", "iPhone 14"]) — on the roadmap.
Cost sanity check
For ~1000 mobile-test runs / month, 5 min average:
| Service | Approx monthly cost |
|---|---|
| BrowserStack ($199/parallel/mo) | ~$200 — fine for low volume |
| Sauce Labs (similar tier) | ~$199 |
T2 self-hosted on AWS c5.metal | ~$500 + ops |
Above ~3 hours/day of test execution, T2 self-hosted becomes more cost-effective. Below that, vendors win on cold-start and zero ops.
What's NOT supported yet
- iOS auto-upload — both vendors accept
.ipavia the same endpoints. The agent currently only runs Android tests, so iOS hasn't been exercised end-to-end. - AWS Device Farm — different execution model (async, not real-time WebDriver). On the roadmap.
- URL caching — every job re-uploads the APK. Future optimization caches the
bs://...URL on the mobile app after first upload.
Troubleshooting
"BrowserStack 401 Unauthorized"
Vault credential is wrong. Check the tag, username, and password against the BrowserStack dashboard.
"credentialTag 'X' not found in environment N's credential vault"
The error message lists available tags. Either fix the tag name in cloud_config or add the missing entry to the env vault.
"No appUrl in cloud_config and no app_path on mobile app"
Either upload an APK first via Upload Binary, or set cloud_config.appUrl to a pre-uploaded vendor URL.
Test runs but device behaves oddly
Check the bstack:options debug capability is on (it's default-true in our config). View the session video in the BrowserStack dashboard.
Next steps
- Self-hosted (T2) — keep data in your infra, avoid vendor lock
- Credentials — full vault setup walkthrough
- YAML format — action reference