~learn.
?
← Case filescheckpoint 1 / 10 · ~23 min left
CASE-009BUILD LOG23 MIN

From Office-Bound Devices to Global Access: The BridgeLink Story

Himanshu Kukrejareads
From Office-Bound Devices to Global Access: The BridgeLink Story
FIG. 1 · Blueprint · CASE-009
watch it first · newThe whole case in a 15-minute animated lecture3 quiz checkpoints · ask the AI about any moment · captions, chapters and speed

Lesson · 22 min

The Problem: The Phones Are on a Desk, the Developers Aren't#

I built BridgeLink while I was founding engineer at NativeBridge. It's a small open-source CLI that does one thing: it makes an Android phone plugged into one computer usable, over plain adb, from any other computer on the internet.

Here's the situation it's built for. (The team below is an illustration, not a customer.)

Picture a five-person Android team in Bangalore building a UPI app that has to work on the phones Indians actually carry: Redmi, Realme, Vivo, Samsung's A-series, plus a couple of Pixels for reference. Over a few years they've bought a shelf of test devices:

FIG 1 · Office device shelf (illustrative)
Office device shelf (illustrative):
├── Samsung Galaxy A-series and S-series
├── Google Pixel (reference devices)
├── Xiaomi Redmi Note, Realme, Vivo, OnePlus
├── A foldable and a tablet or two
└── 50+ phones, several lakh rupees of hardware

Then the team goes hybrid. The shelf is in Koramangala; the developers are in HSR Layout, Whitefield and Indiranagar. A user reports a crash that only happens on one Redmi model. The fix takes ten minutes to write and an afternoon to verify, because verifying it means getting to the office.

The options they have today:

  • Go to the office for every device-specific bug, through Bangalore traffic.
  • Courier phones around and lose days to shipping.
  • Rent devices in a cloud device farm, paying by the minute or by the slot, while their own phones sit idle on the shelf.

What I wanted instead: keep using the phones you already own, and make them reachable from anywhere, as easily as plugging in a USB cable.

ExplainerThe whole story: BridgeLink, remote Android testing

An illustrative team's test phones sit on a shelf while its developers work elsewhere. BridgeLink tunnels adb over the internet: adb tcpip 5555, adb forward to a host port, bore local to bore.pub:15750, then adb connect from anywhere, with a caveat that bore traffic is unencrypted. A tunnel manager, WiFi mode and one-second health and connection monitors turn the weekend script into an early open-source CLI (0.3.0, MIT).

The Vision: Your Physical Devices, Universally Accessible#

DiagramTraditional vs BridgeLink
%% title: Traditional vs BridgeLink
flowchart LR
    subgraph TR["Traditional approach"]
        direction TB
        D1@{ icon: "sd:user", label: "Developer" } --> U@{ icon: "sd:usb", label: "USB cable" } --> P1@{ icon: "sd:phone", label: "One physical device<br/>on the same desk" }
    end
    subgraph BL["BridgeLink approach"]
        direction TB
        D2@{ icon: "sd:laptop", label: "Developer anywhere<br/>Windows · macOS · Linux" } --> I@{ icon: "sd:globe", label: "Internet" }
        I --> T["Authenticated bore tunnel"]:::core
        T --> P2@{ icon: "sd:phone", label: "Physical devices anywhere<br/>Redmi · Realme · Vivo · Pixel" }
    end
    class D1,D2 client
    class U,I edge
    class P1,P2 external

The "Wait, That Should Be Simple!" Moment#

The question that started it: ADB already works over a local network (adb connect 192.168.1.10:5555). So why couldn't it work over the internet?

The Core Insight:

text5 lines
Traditional Approach:
Developer → USB Cable → Physical Device

The Dream:
Developer (Anywhere) → Internet → Secure Tunnel → Physical Device (Anywhere)

The Technical Vision:

  1. Tunneling: Expose ADB over the internet using authenticated bore tunnels
  2. Device Registry: Central platform tracking all connected devices
  3. Auto-Management: Health monitoring, auto-reconnection, state tracking
  4. WiFi Support: Connect devices wirelessly, no USB required
  5. Beautiful CLI: Simple commands that "just work"
  6. Dashboard Control: Web interface for device management and remote sessions

The Research Journey: Learning from the Best#

Before writing a single line of code, I needed to understand the landscape. How were others solving remote device access?

What renting costs (cloud device farm list prices, October 2026):

NUMBERS · label · working · result
AWS Device Farm (metered)$0.17 per device-minute (~₹15/min, ~₹900/hour)
AWS Device Farm (unmetered)$250 per device slot per month (~₹22,000)
Firebase Test Lab$5 per physical-device hour (~₹440)
Sauce Labs Real Device Cloudfrom $199/month, billed yearly (~₹17,500)

(Converted at about ₹88/$. Prices change, so check the vendors' pages.)

Metered pricing is built for short automated runs. Keep one phone open for a working day (8 hours × 20 days) and it's about $1,630 a month, which is why anyone who needs devices all day buys unmetered slots instead. More on the full math below.

The "We Can Build This Better" Moment: What if instead of renting devices in someone else's cloud, we made our own devices remotely accessible? We already own them. We just need a way to reach them over the internet.

The Android Research Deep Dive: The Android ecosystem had already solved device control brilliantly with ADB (Android Debug Bridge)—a tool every Android developer knows and loves.

ADB: The Foundation We Needed:

bash10 lines
# ADB capabilities that changed everything:
adb devices                        # List all connected devices
adb -s SERIAL shell               # Execute shell commands
adb -s SERIAL install app.apk     # Install applications
adb -s SERIAL logcat              # Real-time log streaming
adb connect IP:PORT               # Connect to device over TCP/IP

# The revelation: ADB already supports remote connections!
adb connect 192.168.1.10:5555     # Local network
# But how do we make it work over internet? 🤔

The Missing Piece: Tunneling Enter bore - a modern, fast TCP tunnel that exposes local ports to the internet.

bash7 lines
# bore's beautiful simplicity:
bore local 5555 --to bore.pub
# Output: Listening at bore.pub:15750

# Now ADB can connect globally:
adb connect bore.pub:15750
# Result: Physical device accessible from anywhere! 🎉

A security caveat: bore's --secret only authenticates the handshake. Per bore's README, the tunnel traffic itself is not encrypted, and classic adb tcpip traffic is plaintext too. ADB's own RSA key check still blocks computers the device hasn't authorized, but for anything sensitive, wrap the link in TLS, SSH or a VPN.

The Epiphany:

  • We had: Physical Android devices we already own
  • We had: ADB for complete device control
  • We discovered: bore for simple internet tunneling
  • We needed: Platform to orchestrate everything beautifully

This research phase was crucial - instead of reinventing device control or tunneling from scratch, we could focus on the management layer, health monitoring, and developer experience.

Initial Prototype: Proof of Concept#

Weekend 1 - The Basic Setup:

bash15 lines
#!/bin/bash
# Simple script to expose a device remotely

# Enable ADB TCP mode on USB-connected device
adb -s 1d752b81 tcpip 5555

# Forward local port to device
adb -s 1d752b81 forward tcp:5000 tcp:5555

# Create bore tunnel
bore local 5000 --to bore.pub &

# Output the connection URL
echo "Connect from anywhere:"
echo "adb connect bore.pub:15750"

First success: adb connect came back connected, through a tunnel, to a phone that wasn't plugged into the machine I was typing on. It was manual and fragile, but it worked!

DiagramThe weekend-1 prototype path
%% title: The weekend-1 prototype path
flowchart TD
    DEV@{ icon: "sd:laptop", label: "Developer anywhere<br/>adb connect bore.pub:15750" } --> BP@{ icon: "sd:cloud", label: "bore.pub:15750" }
    BP -->|"bore local 5000 --to bore.pub"| L["Local port 5000"]:::service
    L -->|"adb forward tcp:5000 tcp:5555"| D@{ icon: "sd:phone", label: "Device 1d752b81<br/>adb tcpip 5555" }
    class DEV client
    class BP edge
    class D core

First success: a terminal running adb connect bore.pub:15750 and getting connected, while the Samsung S21 sits in another room on WiFi

The Technical Journey: From Script to Platform#

Challenge 1: The "Tunnel Hell" Problem#

The Problem: Running tunnel processes manually was chaos. Processes died randomly, ports conflicted, and we had no idea which device was on which tunnel.

Failed Approach #1 - Shell Scripts:

bash12 lines
# This quickly became unmaintainable
# tunnel_device_1.sh
# tunnel_device_2.sh
# tunnel_device_3.sh
# ...
# tunnel_device_50.sh 😱

# Problems:
# - No process management
# - No health monitoring
# - No automatic recovery
# - Port conflicts everywhere

The Platform Revolution:

pythonTunnelManager · 33 lines
# Breakthrough: Proper tunnel management with state tracking

class TunnelManager:
    def __init__(self):
        self.tunnels = {}  # session_id -> TunnelInfo
        self.state_file = Path.home() / ".bridgelink" / "tunnels.json"

    def create_tunnel(self, device_serial, adb_port, api_key, device_type):
        """Create and manage bore tunnel lifecycle"""

        # Start bore tunnel subprocess
        process = subprocess.Popen([
            'bore', 'local', str(adb_port),
            '--to', 'bridgelink.nativebridge.io',
            '--secret', api_key
        ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        # Parse tunnel URL from bore output
        tunnel_url = self._parse_tunnel_url(process)

        # Store tunnel info
        tunnel_info = {
            'device_serial': device_serial,
            'url': tunnel_url,
            'pid': process.pid,
            'adb_port': adb_port,
            'device_type': device_type
        }

        # Persist state to survive restarts
        self._save_state()

        return tunnel_info

Results:

  • Before: Manual chaos, frequent failures
  • After: Automated, persistent, recoverable tunnel management
  • Impact: One process that knows every device's tunnel, survives restarts and cleans up after itself

Tunnel hell to tunnel manager: fifty per-device shell scripts replaced by one persisted tunnels.json with each device's tunnel URL, pid, ADB port and type

Challenge 2: WiFi Connection Support#

The USB Cable Problem:

FIG 2 · Reality Check
Reality Check:
├── Remote work: Devices at home, but USB cable length = 6 feet
├── Testing setup: Want device on desk, laptop anywhere in room
├── Multiple devices: USB hub cable management nightmare
└── Flexibility: Need to move devices without reconnecting cables

The WiFi Solution:

pythonWiFiConnectionFlow · 27 lines
# BridgeLink's WiFi magic: 3-step automated setup

class WiFiConnectionFlow:
    def setup_wifi_device(self, usb_serial):
        """Transform USB device to WiFi device in 3 steps"""

        # Step 1: Enable TCP/IP mode on USB device
        subprocess.run(['adb', '-s', usb_serial, 'tcpip', '5555'])
        click.echo("✓ TCP/IP mode enabled on port 5555")

        # Step 2: Get device WiFi IP automatically
        result = subprocess.run(
            ['adb', '-s', usb_serial, 'shell', 'ip', 'route'],
            capture_output=True, text=True
        )
        device_ip = self._parse_ip_from_route(result.stdout)
        click.echo(f"✓ Device IP address: {device_ip}")

        # Step 3: Connect via WiFi
        subprocess.run(['adb', 'connect', f'{device_ip}:5555'])
        click.echo(f"✓ Connected via WiFi: {device_ip}:5555")

        # Now you can disconnect USB cable!
        click.echo("💡 You can now disconnect the USB cable!")
        click.echo("   The device will remain connected via WiFi.")

        return f"{device_ip}:5555"  # New WiFi serial

Real-World Impact:

bash12 lines
# Before WiFi support:
bridgelink devices add 1d752b81  # Must stay connected via USB

# After WiFi support:
bridgelink devices add --wifi 1d752b81
# Output:
# ✓ TCP/IP mode enabled
# ✓ Device IP: 192.168.1.15
# ✓ Connected via WiFi
# 💡 Disconnect USB cable now!

# Device is now wireless, tunnel still works globally! 🎉

USB to WiFi in three steps: adb tcpip 5555 on USB, read the device IP with ip route, then adb connect 192.168.1.15:5555 and unplug the cable

Challenge 3: The "Is My Device Still Alive?" Problem#

The Disconnect Reality:

FIG 3 · Common Scenarios
Common Scenarios:
├── Device battery dies during testing
├── WiFi network temporarily drops
├── ADB daemon crashes on device
├── Developer accidentally unplugs USB
└── Result: Tunnel running, but device is dead 💀

The Health Monitor Solution:

pythonHealthMonitor · 58 lines
class HealthMonitor:
    """Background daemon checking device health every 1 second"""

    def __init__(self, api_key):
        self.api_key = api_key
        self.running = False
        self.check_interval = 1  # Fast 1-second checks

    async def monitor_devices(self):
        """Continuously check all registered devices"""

        while self.running:
            devices = self.api_client.list_devices()

            for device in devices:
                if device['device_state'] != 'active':
                    continue

                # Check if device is actually reachable
                is_healthy = await self._check_device_health(
                    device['device_serial'],
                    device['device_type']
                )

                if not is_healthy:
                    # Device is down! Auto-deactivate
                    logger.warning(f"Device {device['device_serial']} disconnected")

                    # Stop tunnel
                    self.tunnel_manager.stop_tunnel(device['device_serial'])

                    # Update backend state
                    self.api_client.update_device_state(
                        device['device_serial'],
                        'inactive'
                    )

            await asyncio.sleep(self.check_interval)

    async def _check_device_health(self, serial, device_type):
        """Smart health check based on device type"""

        if device_type == 'emulator':
            # Emulators: Check if process is running
            return self._check_emulator_running(serial)
        else:
            # Physical devices: Check ADB connectivity
            try:
                # Run off the event loop, so one slow device can't stall the others
                result = await asyncio.to_thread(
                    subprocess.run,
                    ['adb', '-s', serial, 'shell', 'echo', 'ping'],
                    capture_output=True,
                    timeout=5
                )
            except subprocess.TimeoutExpired:
                return False  # a hung device is an unhealthy device
            return result.returncode == 0

The Auto-Activation Magic:

pythonConnectionMonitor · 47 lines
class ConnectionMonitor:
    """Auto-reconnect devices when they come back online"""

    async def watch_for_reconnections(self):
        """Monitor for devices coming back after disconnect"""

        while self.running:
            # Get all ADB-connected devices
            current_devices = set(ADBDeviceManager.list_devices())

            # Get devices with auto-activation enabled
            auto_activate_devices = self.api_client.get_auto_activate_devices()

            for device in auto_activate_devices:
                device_serial = device['device_serial']

                # Check if device just reconnected
                if device_serial in current_devices:
                    if device['device_state'] == 'inactive':
                        # Device is back! Auto-activate it
                        logger.info(f"Auto-activating {device_serial}")

                        # Create tunnel
                        is_wifi = self._is_wifi_connection(device_serial)
                        adb_port = self.tunnel_manager.setup_adb_tcp(
                            device_serial,
                            is_wifi=is_wifi
                        )
                        tunnel_info = self.tunnel_manager.create_tunnel(
                            device_serial, adb_port, self.api_key,
                            device['device_type']
                        )

                        # Update backend
                        device['device_details']['connection_mode'] = \
                            'WiFi' if is_wifi else 'USB'
                        self.api_client.add_device({
                            'device_serial': device_serial,
                            'device_type': device['device_type'],
                            'device_details': device['device_details'],
                            'tunnel_url': tunnel_info['url'],
                            'device_state': 'active'
                        })

                        logger.info(f"✅ {device_serial} auto-activated!")

            await asyncio.sleep(1)  # Check every second

Real-World Magic:

bash9 lines
# Developer unplugs device accidentally
# 1 second later: Health monitor detects, marks inactive

# Developer plugs device back in
# 1 second later: Connection monitor detects, auto-activates
# Tunnel recreated, backend updated, device ready!

# Total downtime: ~2 seconds
# Developer action required: Zero! 🎉
DiagramHealth monitor and auto-activation
%% title: Health monitor and auto-activation
stateDiagram-v2
    Active: active · tunnel running
    Inactive: inactive · tunnel stopped
    [*] --> Active: bridgelink devices add
    Active --> Inactive: Health monitor · device unreachable · 1 second
    Inactive --> Active: Connection monitor · device back · auto-activate · 1 second
    note right of Inactive
        Battery dies · WiFi drops
        ADB daemon crashes · USB unplugged
    end note
    note left of Active
        Total downtime ~2 seconds
        Developer action required: zero
    end note
    class Active ok
    class Inactive bad
ExplainerBridgeLink: the bore tunnel and self-healing

adb tcpip 5555 puts device 1d752b81 in TCP mode, adb forward maps the host's port 5000 to it, and bore local 5000 --to bore.pub exposes that port as bore.pub:15750, so adb connect bore.pub:15750 works from anywhere. A health monitor pings each active device every second; on an unplug the tunnel stops and the device goes inactive, and the connection monitor recreates the tunnel when it returns: about 2 seconds down, no developer action.

Auto-activation timeline: device unplugged, health monitor marks it inactive a second later, device plugged back in, connection monitor re-activates it a second later

Challenge 4: Beautiful Developer Experience#

The CLI Design Philosophy:

bash19 lines
# Principle 1: Commands should be obvious
bridgelink devices add          # Not: bridgelink register-device
bridgelink devices list         # Not: bridgelink show-all-registered-devices

# Principle 2: Colorful and informative
✅ Authenticated as: you@example.com
📱 Fetching device information...
   Model: Galaxy S21
   Manufacturer: Samsung
   Android: 14 (SDK 34)
✓ Device 1d752b81 is now active!

# Principle 3: Guidance, not errors
❌ No devices found via ADB

Make sure:
  1. Device is connected via USB
  2. USB debugging is enabled
  3. ADB is installed and in PATH

The Color Revolution:

python19 lines
# BridgeLink's color palette for maximum clarity

import click

# Success = Green
click.echo(click.style("✅ Device activated!", fg='green', bold=True))

# Errors = Red
click.echo(click.style("❌ Failed to connect", fg='red', bold=True))

# Warnings/Info = Yellow
click.echo(click.style("⚠️ Security Warning", fg='yellow', bold=True))

# Process Steps = Blue
click.echo(click.style("📡 Setting up WiFi...", fg='blue', bold=True))

# Values/URLs = Cyan/Magenta
click.echo(click.style("192.168.1.15:5555", fg='cyan', bold=True))
click.echo(click.style("bridgelink.io:15750", fg='magenta', bold=True))

The Platform Architecture: How It All Comes Together#

One adb command end to end: developer, NativeBridge platform, bore tunnel with an API-key secret, the device host running the tunnel, health and connection monitors, then the Android device on TCP 5555

DiagramBridgeLink system components
%% title: BridgeLink system components
flowchart TD
    subgraph DT["Developer tier"]
        direction TB
        D1@{ icon: "sd:terminal", label: "CLI Commands<br/>Python Click framework" }
        D2@{ icon: "sd:dashboard", label: "Dashboard Interface<br/>Web-based device management" }
        D3["Remote ADB Access<br/>Global device connectivity"]:::client
        D4["Auto-Activation<br/>Set-and-forget device management"]:::client
        D1 ~~~ D3
        D2 ~~~ D4
    end
    subgraph MT["Management tier"]
        direction TB
        M1["Tunnel Manager<br/>bore tunnel lifecycle management"]:::core
        M2["Health Monitor<br/>1-second device health checks"]:::service
        M3["Connection Monitor<br/>Auto-activation for reconnected devices"]:::service
        M4["Session Persistence<br/>Survive restarts and failures"]:::service
        M5@{ icon: "sd:file", label: "State Management<br/>~/.bridgelink/ state storage" }
        M1 ~~~ M4
        M2 ~~~ M5
    end
    subgraph PT["Platform tier (NativeBridge)"]
        direction TB
        P1@{ icon: "sd:database", label: "Device Registry<br/>Central database of all devices" }
        P2@{ icon: "sd:key", label: "User Authentication<br/>API key-based access control" }
        P3["State Synchronization<br/>Keep device status in sync"]:::service
        P4["Dashboard URL<br/>Web interface for device control"]:::service
        P5["Remote Session API<br/>Start sessions from dashboard"]:::service
        P1 ~~~ P4
        P2 ~~~ P5
    end
    subgraph DVT["Device tier"]
        direction TB
        V1@{ icon: "tech:android", label: "Physical Devices<br/>Samsung, Pixel, OnePlus, etc." }
        V2@{ icon: "sd:usb", label: "ADB Connectivity<br/>USB or WiFi connection" }
        V3["TCP Mode<br/>Port 5555 forwarding"]:::external
        V4["Global Accessibility<br/>via authenticated bore tunnels"]:::external
        V1 ~~~ V3
        V2 ~~~ V4
    end
    DT --> MT --> PT --> DVT
    class D1,D2 client
    class M5,P1 store
    class P2 edge
    class V1,V2 external

Core Components#

1. Command Line Interface (The User's Best Friend):

pythondevices · 39 lines
# Complete CLI surface

@click.group()
def devices():
    """Manage Android devices"""
    pass

@devices.command(name='add')
@click.argument('device_serials', nargs=-1)
@click.option('--wifi', is_flag=True, help='Connect via WiFi')
@click.option('--auto-activate', is_flag=True, help='Auto-reconnect when plugged back in')
async def add_device(device_serials, wifi, auto_activate):
    """Register and activate devices with NativeBridge"""
    # Full WiFi setup flow
    # Tunnel creation
    # Backend registration
    # Health monitoring setup

@devices.command(name='list')
@click.option('--format', type=click.Choice(['table', 'json']))
def list_devices(format):
    """List all registered devices with beautiful table"""
    # Shows: #, Serial, Model, Brand, Type, Mode, State, Auto-Act, Tunnel URL

@devices.command(name='activate')
@click.argument('device_serial')
def activate_device(device_serial):
    """Reactivate existing device"""
    # Check backend registration
    # Create tunnel
    # Update state

@devices.command(name='deactivate')
@click.argument('device_serial', required=False)
@click.option('--all', is_flag=True)
def deactivate_device(device_serial, all):
    """Deactivate devices and stop tunnels"""
    # Stop tunnels
    # Update backend state

2. Background Daemons:

pythonHealthMonitorDaemon · 19 lines
# Health Monitor - The Guardian
class HealthMonitorDaemon:
    """Runs in background, checks devices every 1 second"""

    def start(self, api_key):
        # Fork background process
        # Save PID to ~/.bridgelink/monitor.pid
        # Monitor all active devices
        # Auto-deactivate dead devices

# Connection Monitor - The Auto-Activator
class ConnectionMonitorDaemon:
    """Runs in background, watches for reconnected devices"""

    def start(self, api_key):
        # Fork background process
        # Save PID to ~/.bridgelink/connection_monitor.pid
        # Watch for auto-activate devices
        # Auto-create tunnels when device appears

3. Advanced Features:

python23 lines
# Enhanced device details collection
device_details = {
    'brand': 'Samsung',
    'model': 'Galaxy S21',
    'android_version': '14',
    'sdk_version': '34',
    'security_patch': '2024-10-01',
    'cpu': 'arm64-v8a',
    'ram_gb': '8.0',
    'storage_gb': '128',
    'resolution': '1080x2400',
    'density_dpi': '420',
    'connection_mode': 'WiFi'  # USB or WiFi
}

# Dashboard integration
dashboard_url = get_dashboard_url()
# dev: trust-me-bro.nativebridge.io/dashboard/bridgelink
# prod: nativebridge.io/dashboard/bridgelink

# Smart session detection
# If only 1 device: auto-select
# If multiple: show list to choose

What It Could Save: The Honest Math#

BridgeLink is young, and I don't have adoption numbers to show. What I can show is the math for a team that adopts it, using public list prices. These are potential savings, not measured results.

Scenario: a 10-phone team#

FIG 4 · Renting 10 phones for remote access, all day, every working day
Renting 10 phones for remote access, all day, every working day:
├── AWS Device Farm, unmetered: 10 slots × $250  = $2,500/month (~₹2.2 lakh)
├── Per year:                                      $30,000 (~₹26 lakh)
└── Metered instead: ~$1,630 per phone-month     → ~$16,300/month for 10

Using 10 phones the team already owns, with BridgeLink:
├── Phones:        already on the shelf (or ~₹15,000-40,000 each to buy)
├── Host machine:  any spare laptop or mini PC with USB ports
├── BridgeLink:    free and open source (MIT)
└── Relay:         a NativeBridge account for the tunnel and dashboard

Potential saving: up to roughly ₹25 lakh a year for one 10-phone team, less the relay and any hardware you buy. Even buying all ten phones new (₹1.5–4 lakh) costs about two months of slot rent.

And at scale: if 50 teams like that used their own phones instead of renting slots, that's on the order of ₹13 crore a year staying in their budgets.

Where a device farm still wins#

Being honest about the trade-off is part of the design:

  • Breadth: farms offer hundreds of models you'll never buy. BridgeLink only reaches phones you own.
  • Maintenance: someone else replaces swollen batteries, updates OS versions and keeps devices charged.
  • Parallel CI at scale: running one suite on 50 models at once is what farms are built for.
  • Security: a farm is a managed service. With BridgeLink you're responsible for the host machine, the API key, and wrapping traffic in TLS, SSH or a VPN when it matters (see the caveat above).

So BridgeLink is for the daily loop: the phones you test on every day, which you've already paid for. Rent a farm for the long tail.

What the illustrative team gets back#

Back to the Bangalore team from the opening, with their shelf of phones on BridgeLink:

Timeline
  1. 10:00 AMCrash report on a Redmi Note
  2. 10:05 AMbridgelink devices list (finds the Redmi's tunnel URL)
  3. 10:06 AMadb connect bridgelink.nativebridge.io:15750
  4. 10:07 AMDebugging the real Redmi from home in HSR Layout

No cab, no courier, and no slot rent for a phone that's already on their shelf.

Technical Deep Dive: The Magic Behind the Scenes#

WiFi Connection Flow#

pythonWiFiConnectionFlow · 52 lines
class WiFiConnectionFlow:
    """Complete WiFi setup automation"""

    def setup_wifi_connection(self, usb_serial):
        """Transform USB device to WiFi device"""

        # Step 1: Enable TCP/IP mode
        click.echo(click.style("   Step 1/3: Enabling TCP/IP mode...", fg='blue'))
        result = subprocess.run(
            ['adb', '-s', usb_serial, 'tcpip', '5555'],
            capture_output=True, timeout=10
        )
        if result.returncode != 0:
            raise WiFiSetupError("Failed to enable TCP/IP mode")

        click.echo(click.style("   ✓ TCP/IP mode enabled on port 5555", fg='green'))
        time.sleep(3)  # Wait for mode switch

        # Step 2: Get device IP
        click.echo(click.style("   Step 2/3: Getting device IP address...", fg='blue'))
        result = subprocess.run(
            ['adb', '-s', usb_serial, 'shell', 'ip', 'route'],
            capture_output=True, text=True, timeout=10
        )

        device_ip = self._parse_ip_address(result.stdout)
        if not device_ip:
            raise WiFiSetupError("Could not determine device IP address")

        click.echo(click.style("   ✓ Device IP address: ", fg='green') +
                   click.style(device_ip, fg='cyan', bold=True))

        # Step 3: Connect via WiFi
        click.echo(click.style("   Step 3/3: Connecting via WiFi...", fg='blue'))
        result = subprocess.run(
            ['adb', 'connect', f'{device_ip}:5555'],
            capture_output=True, timeout=10
        )

        if result.returncode != 0:
            raise WiFiSetupError("Failed to connect via WiFi")

        click.echo(click.style(f"   ✓ Connected via WiFi: ", fg='green') +
                   click.style(f"{device_ip}:5555", fg='cyan', bold=True))

        # Success message
        click.echo(click.style("\n💡 You can now disconnect the USB cable!",
                              fg='yellow', bold=True))
        click.echo(click.style("   The device will remain connected via WiFi.",
                              fg='yellow'))

        return f"{device_ip}:5555"
DiagramWiFi connection flow
%% title: WiFi connection flow
flowchart TD
    U(["USB-connected device"]):::client --> S1["Step 1/3 · adb tcpip 5555"]:::service
    S1 -->|"returncode != 0"| E1(["WiFiSetupError<br/>Failed to enable TCP/IP mode"]):::bad
    S1 -->|"wait 3s for mode switch"| S2["Step 2/3 · adb shell ip route<br/>parse device IP"]:::service
    S2 -->|"no IP"| E2(["WiFiSetupError<br/>Could not determine device IP address"]):::bad
    S2 --> S3["Step 3/3 · adb connect IP:5555"]:::core
    S3 -->|"returncode != 0"| E3(["WiFiSetupError<br/>Failed to connect via WiFi"]):::bad
    S3 --> OK(["Connected via WiFi<br/>You can now disconnect the USB cable"]):::ok

Auto-Activation Intelligence#

pythonSmartAutoActivation · 65 lines
class SmartAutoActivation:
    """Intelligent device reconnection"""

    async def handle_device_reconnection(self, device_serial):
        """Auto-activate device with full context awareness"""

        # Get device info from backend
        device = self.api_client.get_device(device_serial)

        if not device:
            logger.warning(f"Unknown device {device_serial} connected")
            return

        if not device.get('auto_activate'):
            logger.info(f"Device {device_serial} auto-activate disabled")
            return

        if device['device_state'] == 'active':
            logger.info(f"Device {device_serial} already active")
            return

        logger.info(f"🔄 Auto-activating {device_serial}...")

        # Detect connection mode
        is_wifi = self._is_wifi_connection(device_serial)
        connection_mode = 'WiFi' if is_wifi else 'USB'

        # Setup ADB TCP port forwarding
        adb_port = self.tunnel_manager.setup_adb_tcp(
            device_serial,
            is_wifi=is_wifi
        )

        if not adb_port:
            logger.error(f"Failed to setup ADB for {device_serial}")
            return

        # Create bore tunnel
        tunnel_info = self.tunnel_manager.create_tunnel(
            device_serial,
            adb_port,
            self.api_key,
            device['device_type']
        )

        if not tunnel_info:
            logger.error(f"Failed to create tunnel for {device_serial}")
            return

        # Update device details with current connection mode
        device_details = device['device_details']
        device_details['connection_mode'] = connection_mode

        # Update backend
        self.api_client.add_device({
            'device_serial': device_serial,
            'device_type': device['device_type'],
            'device_details': device_details,
            'tunnel_url': tunnel_info['url'],
            'device_state': 'active'
        })

        logger.info(f"✅ {device_serial} auto-activated!")
        logger.info(f"   Tunnel: {tunnel_info['url']}")
        logger.info(f"   Mode: {connection_mode}")

Enhanced Device Information Collection#

pythonDeviceInformationCollector · 47 lines
class DeviceInformationCollector:
    """Collect comprehensive device details via ADB"""

    def collect_device_info(self, serial):
        """Get all device information in one go"""

        # Basic info
        model = self._get_prop(serial, 'ro.product.model')
        manufacturer = self._get_prop(serial, 'ro.product.manufacturer')
        android_version = self._get_prop(serial, 'ro.build.version.release')
        sdk_version = self._get_prop(serial, 'ro.build.version.sdk')

        # Enhanced info
        security_patch = self._get_prop(serial, 'ro.build.version.security_patch')
        cpu = self._get_prop(serial, 'ro.product.cpu.abi')

        # RAM info
        ram_result = subprocess.run(
            ['adb', '-s', serial, 'shell', 'cat', '/proc/meminfo'],
            capture_output=True, text=True
        )
        ram_gb = self._parse_ram(ram_result.stdout)

        # Storage info
        storage_result = subprocess.run(
            ['adb', '-s', serial, 'shell', 'df', '/data'],
            capture_output=True, text=True
        )
        storage_gb = self._parse_storage(storage_result.stdout)

        # Screen info
        resolution = self._get_resolution(serial)
        density_dpi = self._get_density(serial)

        return DeviceInfo(
            serial=serial,
            model=model,
            android_version=android_version,
            sdk_version=sdk_version,
            manufacturer=manufacturer,
            security_patch=security_patch,
            cpu=cpu,
            ram_gb=ram_gb,
            storage_gb=storage_gb,
            resolution=resolution,
            density_dpi=density_dpi
        )

Lessons Learned: What Worked and What Didn't#

Technical Wins#

1. WiFi Support Was Game-Changing:

text2 lines
USB-only approach: Devices tethered to the host, cable management nightmare
WiFi support: Devices anywhere on the network, clean setup, ultimate flexibility

2. Auto-Activation Eliminated Friction:

python9 lines
# Without auto-activation:
# Device disconnects → Manual: bridgelink devices activate SERIAL
# Device reconnects → Manual: bridgelink devices activate SERIAL

# With auto-activation:
# Device disconnects → Auto-deactivated (1 second detection)
# Device reconnects → Auto-activated (1 second detection)

# Developer action required: Zero! 🎉

3. A Colourful CLI Makes the Important Line Stand Out:

bash4 lines
# Before colors: Plain white text, hard to scan
# After colors: Success = green, errors = red, info = yellow

# Result: the one red line in a wall of output is the one you notice

Technical Challenges Overcome#

1. Tunnel URL Parsing Complexity:

pythonparse_tunnel_url · 18 lines
# Challenge: bore output format varies by version
# Solution: Robust regex parsing with fallbacks

def parse_tunnel_url(self, bore_output):
    """Parse tunnel URL from various bore output formats"""

    patterns = [
        r'listening at ([a-z0-9\.\-]+:\d+)',  # v0.4.0+
        r'connected to ([a-z0-9\.\-]+:\d+)',  # v0.3.x
        r'tunnel to ([a-z0-9\.\-]+:\d+)'      # v0.2.x
    ]

    for pattern in patterns:
        match = re.search(pattern, bore_output, re.IGNORECASE)
        if match:
            return match.group(1)

    raise TunnelParseError("Could not parse tunnel URL")

2. Cross-Platform Path Issues:

python9 lines
# Challenge: Windows vs macOS/Linux path differences
# Solution: pathlib for everything

from pathlib import Path

# Works everywhere
STATE_DIR = Path.home() / ".bridgelink"
MONITOR_PID = STATE_DIR / "monitor.pid"
MONITOR_LOG = STATE_DIR / "monitor.log"

3. Process Management Reliability:

pythonTunnelManager · 19 lines
# Challenge: Zombie processes, orphaned tunnels
# Solution: Proper cleanup with signal handlers

import signal
import atexit

class TunnelManager:
    def __init__(self):
        self.tunnels = {}

        # Register cleanup handlers
        atexit.register(self.cleanup_all)
        signal.signal(signal.SIGTERM, self.signal_handler)
        signal.signal(signal.SIGINT, self.signal_handler)

    def cleanup_all(self):
        """Ensure all tunnels stopped on exit"""
        for serial in list(self.tunnels.keys()):
            self.stop_tunnel(serial)

What Didn't Work Initially#

1. Real Device Streaming:

python3 lines
# Attempted: Stream device screen like iOS Bridge
# Problem: a full streaming stack was out of scope (scrcpy already streams over ADB, no root needed)
# Decision: Focus on ADB access, let developers use scrcpy separately

2. Automatic bore Installation:

python3 lines
# Attempted: Auto-install bore during pip install
# Problem: Platform-specific binaries, permission issues
# Solution: bridgelink setup command with clear instructions

3. Multiple Simultaneous Tunnels Per Device:

python3 lines
# Attempted: Create multiple tunnels for same device
# Problem: ADB port conflicts, confusing for users
# Solution: One tunnel per device, clear ownership

Current Development Focus#

Performance Optimization:

Goals for next version
  • Faster tunnel creation (< 2 seconds)
  • More efficient health checks
  • Reduced memory footprint
  • Better network reliability

Advanced Features Pipeline:

Planned features
  • Device pools — share devices across teams
  • Reservation system — book devices for testing windows
  • Integration with CI/CD — Jenkins, GitHub Actions
  • Team management — organization accounts
  • Analytics dashboard — usage metrics, uptime stats

Roadmap Ideas (No Dates Promised)#

  • Stability: better error handling, tunnel recovery and logging
  • Sharing: multi-user device pools and reservations
  • Teams: organisation accounts, SSO, role-based access and audit logs
  • CI/CD: run test suites against BridgeLink devices from GitHub Actions or Jenkins

Contributing#

The Project#

Project Stats:

FIG 5 · Tree
├── GitHub Repository: https://github.com/himanshkukreja/bridgelink
├── Current Version: 0.3.0 (PyPI), also on Homebrew
├── License: MIT (fully open source)
├── Architecture: Python + Click + bore, with NativeBridge as relay and dashboard
└── Platforms: macOS, Linux, Windows

Community Needs:

python18 lines
# We especially need help with:
platform_support = [
    "Windows installer improvements",
    "Linux package managers (apt, yum)",
    "macOS Homebrew formula maintenance"
]

features = [
    "CI/CD integrations",
    "Team collaboration features",
    "Advanced device pooling"
]

documentation = [
    "Video tutorials",
    "Corporate deployment guides",
    "Troubleshooting database"
]

How to Get Involved#

For Users:

bash12 lines
# Try it yourself:
pip install bridgelink   # or: brew tap AutoFlowLabs/tap && brew install bridgelink

# Quick setup:
bridgelink setup  # Installs ADB and bore

# Register your first device:
export NB_API_KEY="your-api-key"   # from the NativeBridge dashboard
bridgelink devices add

# View your devices:
bridgelink devices list

For Contributors:

bash5 lines
# Join the development:
git clone https://github.com/himanshkukreja/bridgelink
cd bridgelink
pip install -r requirements.txt
pip install -e .
Areas needing help
  • CLI enhancements and new commands
  • Cross-platform compatibility testing
  • Tunnel management improvements
  • Documentation and examples
  • Integration with popular CI/CD platforms

Conclusion: Use the Phones You Already Own#

It's an early, small open-source project: version 0.3.0 on PyPI and Homebrew, MIT licensed, built while I was founding engineer at NativeBridge. It does the core loop well: register a phone, tunnel it, keep it healthy, and bring it back on its own after a reconnect. Everything else is on the roadmap above.

What It Could Become#

The idea is bigger than the tool. Most Indian app teams already own the phones their users carry, and most of those phones spend their day on a shelf. Made reachable, they could serve:

FIG 6 · Tree
├── Remote and hybrid teams testing on real hardware without a trip to the office
├── QA engineers in Pune or Hyderabad using phones that live in Bangalore
├── Small teams keeping ₹2 lakh+ a month of slot rent (for 10 phones) in their budget
├── Budget phone testing (Redmi, Realme, Vivo) from anywhere
└── Individual developers sharing a test phone with a collaborator

Personal Reflection#

Building BridgeLink taught me that the best tools often sit on top of something that already works. ADB was already a remote protocol; bore was already a tunnel. The real work was in the unglamorous parts: tunnel lifecycles, health checks, reconnects and clear output. That's where a weekend script becomes something you can hand to a teammate.

If you try it, I'd love to hear what breaks.


Get started: pip install bridgelink, bridgelink setup, export NB_API_KEY, bridgelink devices add, bridgelink devices list

Want to try it?

  • 🚀 Try it: pip install bridgelink (or brew tap AutoFlowLabs/tap && brew install bridgelink)
  • 🔧 Contribute: GitHub Repository
  • 📖 Learn: Read the full documentation
  • 💬 Tell me: what your team would need before it could use BridgeLink

Built for:

  • 🏢 Bangalore/Pune/Hyderabad teams with a shelf of test phones and a distributed team
  • 🏠 Remote developers testing on budget phones (Redmi, Realme, Vivo)
  • 💰 Cost-conscious teams who'd rather not rent what they already own
  • 🚀 Individual developers building for Bharat

BridgeLink: the phones on your desk, usable from anywhere.

End of file · CASE-009

Read it through to close the case.

0/10 sections read. Each one counts after a few minutes of actual reading in it; skipping ahead doesn't. The case closes itself once every section is read.

All case files →
> debrief --interactiveCASE-009 · +25 XP at 8/11
End-of-file activity · fill the blanks

Rebuild Priya's weekend prototype port by port, move a phone off its USB cable, then keep it alive when the battery dies. Four quick steps, all from this case file.

  • ~2 MIN
  • 4 STEPS · 11 PTS
  • SOLVE AT 8/11
  • +25 XP

this case in shorts · 2

play them all →

One idea from this case in about a minute each, made for your phone.

Android DevelopmentRemote DevelopmentADBDeveloper ExperienceTestingCI/CDAutomationOpen SourceTech InnovationProductivity

Discussion

Add to the post-mortem
loading the record…