Run Maestro with the TestingBot Tunnel
TestingBot runs your Maestro flows on real devices, simulators and emulators in our cloud datacenter. Those devices cannot, by default, reach an app or API that lives on your machine, your CI runner or a private staging network. The TestingBot Tunnel bridges that gap: it opens a secure, SSH-encrypted connection so the device under test can reach your internal hosts as if it were sitting on your own network.
The easiest way to use a tunnel with Maestro is the -t flag on the TestingBot CLI. It starts a tunnel, runs your flows through it, and tears the tunnel down when the run completes.
When you need a tunnel
Use a tunnel whenever your Maestro flow drives an app that talks to a backend the public internet cannot see. Common scenarios:
-
Staging and pre-production backends.
Your app points at a staging API (
https://staging.internal.example.com) that is firewalled off from the public internet. The tunnel lets the cloud device resolve and reach it. -
Localhost development servers.
You are iterating locally and your API runs on
http://localhost:3000on your laptop or CI runner. The tunnel forwards that port to the device. -
Protected or token-gated endpoints.
Your backend requires a session or bearer token that is only issued inside your network. A small helper script can
fetchthat token from a protected endpoint over the tunnel at runtime, so the secret never lives in your flow files. - Private databases and internal services. The app calls an internal microservice, payment sandbox or database proxy that has no public DNS entry.
- Mock and contract-test servers. You run a mock server (WireMock, Mockoon, MSW) on your CI runner to return deterministic responses; the tunnel exposes it to the device for stable, hermetic tests.
- Geo or VPN-restricted APIs. An upstream API only accepts traffic from your office or VPN egress IP. Routing through the tunnel makes requests originate from your network.
If your backend is already publicly reachable (a deployed staging URL with valid TLS and no IP allowlist), you do not need a tunnel. The tunnel is only required when the device cannot otherwise reach the host.
Quick Start
Add the -t flag (short for --tunnel) to your normal Maestro run command. The CLI starts a tunnel before the run and stops it afterwards:
testingbot maestro app.apk ./flows \
--device "Pixel 8" \
--deviceVersion "14" \
-t
That single command will:
- Authenticate and start a TestingBot Tunnel for this run
- Upload your app and Maestro flows
- Run the flows, routing all device traffic through the tunnel
- Shut the tunnel down once the run finishes
| Flag | Description |
|---|---|
-t, --tunnel |
Start a TestingBot Tunnel for this test run |
--tunnel-identifier <id> |
Identifier for the tunnel, allowing multiple tunnels to run in parallel |
The -t flag cannot be combined with --async. The tunnel has to stay open for the whole run, so the CLI must wait for the flows to finish.
Prefer to manage the tunnel yourself (for example, to keep one tunnel up across many runs)? Start the TestingBot Tunnel client separately and leave it running. When a tunnel is active on your account, Maestro runs automatically route through it, with no extra flags needed.
How it works
When the tunnel is up, the cloud device sends its network requests over the encrypted tunnel to the tunnel client running in your environment. The client forwards each request to the target host (your localhost, staging server or internal API) and relays the response back to the device.
Because the request originates from inside your network, internal DNS, VPN routes and IP allowlists all behave as they normally would for local traffic. The tunnel supports HTTP/1, HTTP/2, WebSocket and Server-Sent Events, so streaming and real-time backends work as well.
For WebSocket or Server-Sent Events backends, start the standalone tunnel client with the --nobump --nocache flags. See the tunnel command reference.
Running multiple tunnels
If you run several Maestro builds in parallel, give each one its own named tunnel so traffic routes to the correct environment.
Use --tunnel-identifier to set a unique name:
# Build A, pointing at the "staging" tunnel
testingbot maestro app.apk ./flows/smoke \
--device "Pixel 8" \
-t --tunnel-identifier staging
# Build B, pointing at the "qa" tunnel
testingbot maestro app.apk ./flows/regression \
--device "iPhone 16" --real-device \
-t --tunnel-identifier qa
Without an identifier, all traffic flows through the single default tunnel on your account. See running multiple tunnels for more detail.
Staging token helper
A very common need: your staging or pre-production backend is protected and the app needs a short-lived auth token before it can do anything useful. You do not want to hardcode that token (it rotates, and committing secrets to flow files is unsafe). Instead, fetch it at runtime from a token endpoint that is only reachable over the tunnel.
Maestro can run inline JavaScript with the runScript command. The script executes in Maestro's JS environment and can make an HTTP request to your protected backend, which is reachable because the device routes through the tunnel.
Store the result in output so later steps can use it.
1. The helper script
Create a small script, for example scripts/fetch_token.js, that calls your protected token endpoint:
// scripts/fetch_token.js
// Runs inside Maestro. The request reaches your staging backend
// through the TestingBot Tunnel, so localhost / internal hosts resolve.
var response = http.post('http://localhost:3000/internal/auth/token', {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client: 'maestro-e2e',
scope: 'staging'
})
})
var token = json(response.body).access_token
// Expose the token to the rest of the flow
output.authToken = token
Maestro exposes an http helper and a json() parser inside runScript. If your backend is on your laptop, the host is localhost; for a named staging host, use its internal hostname (for example http://staging.internal:8080) and it will resolve over the tunnel.
2. Call it from your flow
Run the helper near the start of the flow, then use the token in subsequent steps, for example to deep-link into an authenticated screen or to set it in app state:
appId: com.example.app
---
- runScript: scripts/fetch_token.js
# Use the token captured by the helper.
# For example, open a deep link that carries the session token:
- openLink: "exampleapp://session?token=${output.authToken}"
- assertVisible: "Welcome back"
3. Run it through the tunnel
Run as usual with the -t flag so localhost (or your staging host) is reachable from the device:
testingbot maestro app.apk ./flows \
--device "Pixel 8" \
--deviceVersion "14" \
-t
Keep the token endpoint scoped to non-production data and issue short-lived tokens. The tunnel is encrypted end to end, but treat any credential the flow can read as test-grade only.
The same pattern works for any runtime value the device needs from a protected source: feature-flag overrides, a seeded test-user id, a one-time passcode from a mailbox API, or a CSRF token.
Using the tunnel in CI/CD
The -t flag behaves identically in CI. Build your app, then run Maestro with the tunnel so the cloud device can reach the mock server or staging backend running on the CI runner:
npx --yes @testingbot/cli@latest maestro \
app/build/outputs/apk/debug/app-debug.apk \
./maestro-flows \
--device "Pixel 8" \
--deviceVersion "14" \
-t \
--report junit \
--report-output-dir ./test-results
For full GitHub Actions, GitLab CI, Jenkins, CircleCI and Bitbucket Pipelines examples, including secret handling and reports, see the Maestro CI/CD integration page.
Because -t cannot be combined with --async, the CI step blocks until the run finishes, which is what you want so the pipeline can pass or fail on the result.
CLI Reference
| CLI Option | Description |
|---|---|
-t, --tunnel |
Start a TestingBot Tunnel for this test run (cannot be combined with --async) |
--tunnel-identifier <id> |
Identifier for the tunnel, allowing multiple tunnels to run in parallel |
See the full Maestro options and the TestingBot Tunnel documentation for advanced settings such as upstream proxies, custom DNS, header injection and PAC support.
Frequently asked questions
Why do I need a tunnel to run Maestro on TestingBot?
TestingBot devices run in our cloud datacenter and cannot reach apps or APIs that live on your laptop, your CI runner or a private staging network. The TestingBot Tunnel opens a secure, SSH-encrypted connection so the device under test can resolve and reach those internal hosts as if it were on your own network. See the use cases section for common scenarios.
How do I start a tunnel for a Maestro run?
Pass the -t (or --tunnel) flag to the TestingBot CLI. The CLI starts a tunnel, runs your flows through it, and shuts the tunnel down when the run finishes. See the quick start.
Can I run multiple Maestro tunnels in parallel?
Yes. Give each tunnel a unique name with --tunnel-identifier <id> so parallel runs route through the correct tunnel. See running multiple tunnels.
How does my app get a staging auth token during a Maestro flow?
Your app, running on the cloud device, makes its normal network calls through the tunnel back to your protected backend. A small helper that performs a fetch/AJAX call to a token endpoint behind the tunnel can retrieve a short-lived token at runtime, so you never hardcode secrets in the flow. See the token helper example.
Can I use the tunnel in CI/CD?
Yes. The -t flag works the same way in any CI runner. Combine it with the examples on the Maestro CI/CD page. The -t flag cannot be combined with --async, because the tunnel must stay up for the duration of the run.