Horizon

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 your API

Start your API as you always do.

npm run dev

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-app
Output
HORIZON: Tunnel connected
  URL          https://my-app.hrzn.run (reserved)
  Forwarding   http://localhost:3000
  Request log  https://hrzn.run/dashboard/tunnels/my-app

Set 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.

.env
EXPO_PUBLIC_API_URL=https://my-app.hrzn.run

Read it with static dot notation. Expo doesn't replace process.env["EXPO_PUBLIC_API_URL"] or destructured access.

src/api.ts
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.

src/PreviewScreen.tsx
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. An https:// 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:

Output
  GET     200  /api/profile

Open 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 for Reconnected.
  • Check that your API runs on the port in the hrzn tunnel command.

Next steps

On this page