Test a mobile app against your local API
Point an iOS, Android, React Native or Expo app at your local API through a Horizon HTTPS tunnel, without device network setup.
Point an iOS, Android, React Native or Expo app at the API on your laptop. The tunnel gives your phone or emulator a public HTTPS URL.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- An API running on your machine, for example on
http://localhost:3000 - A mobile app you can rebuild or reload with a new base URL
- Optional: a reserved subdomain for
-s, so the URL in your app doesn't change. Reserve one on the Subdomains page.
Start a tunnel
Without -s the subdomain is random and changes every run. Your app would then point at a dead URL after a restart. Use a reserved subdomain, see Pricing.
hrzn tunnel http://localhost:3000 -s my-appHORIZON: Tunnel connected
URL https://my-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-appSet the base URL in your app
Replace http://localhost:3000 with https://my-app.hrzn.run. A phone can't reach localhost on your laptop. The tunnel URL works from any network.
In Expo, put the URL in a variable with the EXPO_PUBLIC_ prefix. Expo inlines these into your bundle, so never put a secret in one.
EXPO_PUBLIC_API_URL=https://my-app.hrzn.runRead it with static dot notation. Expo doesn't replace process.env["EXPO_PUBLIC_API_URL"] or destructured access.
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
export async function getProfile() {
const response = await fetch(`${apiUrl}/api/profile`, {
headers: { Accept: "application/json" },
});
return response.json();
}After you change .env, do a full reload of the app (shake gesture, then Reload) to see the new value.
Add the skip header for WebViews
A native HTTP client, such as fetch in React Native, URLSession or OkHttp, usually sends a non-browser user agent. Horizon doesn't show the Before you continue page to those requests.
A WebView sends a browser user agent. So does any client you set to a browser user agent. Add an x-hrzn-skip-warning header to those requests. Any value works.
With react-native-webview, pass the header in the source prop.
import { WebView } from "react-native-webview";
export function PreviewScreen() {
return (
<WebView
source={{
uri: "https://my-app.hrzn.run",
headers: { "x-hrzn-skip-warning": "1" },
}}
/>
);
}react-native-webview sends these headers with the first request only. On Android it supports them for GET requests only.
iOS and Android security rules
You don't need to change either platform's network settings, because the tunnel is HTTPS.
- iOS: App Transport Security (ATS) is on by default. It requires secure connections: TLS 1.2 or later, strong ciphers and forward secrecy. A plain
http://URL fails unless you add an exception. Anhttps://tunnel URL doesn't need one. - Android: Apps that target Android 9 (API level 28) or later block cleartext HTTP by default. HTTPS needs no configuration.
Don't set NSAllowsArbitraryLoads or cleartextTrafficPermitted="true" to reach a tunnel. You don't need them, and Apple triggers App Store review for NSAllowsArbitraryLoads.
Check it works
Trigger a request in your app. Your Horizon terminal prints one line per request:
GET 200 /api/profileOpen the Request log link to see recent requests in the dashboard.
Troubleshooting
My app gets an HTML page instead of JSON
The request got the Before you continue page. Your client sends a browser user agent. Add the x-hrzn-skip-warning header to the request. See the WebView step above.
The request fails on iOS with an App Transport Security error
Your app still calls an http:// URL. Use the https:// tunnel URL. Check that nothing hard-codes the old http://localhost base URL.
The request fails on Android with a cleartext error
Your app still calls an http:// URL. Use the https:// tunnel URL.
The app still calls the old URL
Do a full reload after you change .env. In Expo, a variable needs the EXPO_PUBLIC_ prefix and static process.env.EXPO_PUBLIC_API_URL access.
The URL changed after a restart
You started the tunnel without -s. Restart with -s my-app. -s needs a subdomain you reserved, see Pricing.
Nothing reaches your API
- Check that the Horizon terminal is still running. If its last line is
Connection lost. Reconnecting…, wait forReconnected. - Check that your API runs on the port in the
hrzn tunnelcommand.
Next steps
Share a local API
Share a local API with curl, Postman and browser fetch calls from another origin, using a Horizon tunnel and the x-hrzn-skip-warning header.
Expose a local WebSocket server
Expose a local WebSocket server with a Horizon tunnel and connect to it over wss from a browser, a phone or another machine.