expo-air

Remote Development

Tunnels, fly command, and developing from anywhere

Why tunnels?

By default, Metro serves your app over the local network. When your device isn't on the same WiFi — or you're developing from a coffee shop, testing on a remote device, or sharing a dev build — you need a tunnel to bridge the connection.

expo-air uses Cloudflare tunnels by default, with automatic fallback providers.

Getting started

Connect your device and run:

npx expo-air@latest fly

This detects your device, builds the app, installs it, and starts everything with tunnels in one command.

Tunnel providers

ProviderStatusNotes
CloudflareDefaultFree, fast, recommended
localtunnelFallbackUsed if Cloudflare is rate-limited
boreFallbackAlternative fallback

expo-air automatically falls back to the next provider if the primary one fails or is rate-limited.

Tunnel configuration

.expo-air.local.json

When tunnels are created, expo-air writes the URLs to .expo-air.local.json:

{
  "serverUrl": "wss://random-name.trycloudflare.com",
  "widgetMetroUrl": "https://random-name.trycloudflare.com",
  "appMetroUrl": "https://random-name.trycloudflare.com"
}

This file is:

  • Gitignored — never committed
  • Regenerated every time you run fly
  • Read by the config plugin which writes the URLs into Info.plist during prebuild

ATS exceptions

The config plugin automatically adds App Transport Security exceptions for all tunnel domains:

  • trycloudflare.com
  • loca.lt
  • bore.pub

No manual Info.plist editing needed.

Extra tunnels

If your app connects to local services (API server, backend, etc.), you can tunnel those too. Add extraTunnels and envFile to your .expo-air.json:

{
  "autoShow": true,
  "ui": {
    "bubbleSize": 60,
    "bubbleColor": "#000000"
  },
  "envFile": ".env.local",
  "extraTunnels": [
    {
      "port": 3000,
      "name": "API Server",
      "envVar": "EXPO_PUBLIC_API_URL"
    },
    {
      "port": 5000,
      "name": "Backend",
      "envVar": "EXPO_PUBLIC_BACKEND_URL"
    }
  ]
}

Each entry in extraTunnels creates a Cloudflare tunnel for the specified local port. The tunnel URL is written to the env file under the given envVar name.

Extra tunnel config

FieldTypeDescription
portnumberLocal port to tunnel
namestringFriendly name shown in logs
envVarstringEnvironment variable to write the tunnel URL to

Result

When you run expo-air fly, the env file is auto-generated:

# .env.local (auto-generated by expo-air)
EXPO_PUBLIC_API_URL=https://xyz.trycloudflare.com
EXPO_PUBLIC_BACKEND_URL=https://abc.trycloudflare.com

Your app can then read these URLs via process.env.EXPO_PUBLIC_API_URL to connect to your local services through the tunnel.

Connection resilience

The HMR auto-reconnect module handles tunnel instability automatically:

  • Reconnects with exponential backoff when the connection drops
  • Replays Metro registration messages to restore the HMR session
  • Notifies the prompt server to re-trigger file updates

This means tunnel drops, WiFi transitions, and app backgrounding are handled transparently — your development session resumes without manual intervention.

Troubleshooting tunnels

IssueSolution
Cloudflare rate limitWait a few minutes
Tunnel won't connectCheck your internet connection; try restarting with expo-air fly
Slow tunnel performanceCloudflare tunnels are fastest; ensure you're not falling back to localtunnel
URLs expiredTunnel URLs change every session — just restart fly