This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

Show an Expo Simulators session on your own web page

Edit page

Embed the browser preview of an Expo Simulators session in an iframe, or render the device screen in your own React app with @expo/hub-client.


You can show a running Expo Simulators session on a web page that you own, such as an internal tool, a dashboard, or a product demo. There are two methods:

MethodWhat your page showsFlag on eas simulator:start
Embed the preview in an iframeThe complete preview page of the preview host--frame-ancestor <origin>
Render the screen with @expo/hub-clientOnly the device screen, inside your own React layout--cors-origin <origin>

By default, only expo.dev can embed the preview or call the preview host's API. Each flag adds the origin of your page to an allow list for one session. The Expo Simulators embed example is a small Expo web app that shows both methods. It runs at simulator-embed-example.expo.app.

Choose the origins

Pass the origin of each page that shows the session. An origin is the scheme, host, and port of a URL, without a path. For example, the origin of https://tools.example.com/qa/run is https://tools.example.com.

  • Use https. Expo accepts plain http only for localhost, 127.0.0.1, and [::1], so you can test with a local development server.
  • To allow all subdomains of a domain, use a wildcard such as https://*.example.com. The wildcard needs at least two labels after the star, so Expo rejects https://*.com.
  • Each flag takes one origin. Repeat a flag up to 10 times to allow more origins.

EAS CLI rejects a value that is not an origin before it creates the session. For example, --frame-ancestor https://tools.example.com/qa returns an error that shows the correct value, https://tools.example.com.

Set the origins when you start the session.

Get the preview host URL and token

When you pass --frame-ancestor or --cors-origin, EAS CLI prints the preview host URL with the preview token, even for a session with private links. In --json output, the value is remoteConfig.previewApiUrl:

Terminal
- eas simulator:start --platform ios --frame-ancestor https://tools.example.com --max-duration-minutes 30 --json

The value has this format:

https://<preview-host>/?token=<preview-token>

eas simulator:get does not include this value for a session with private links. Save it from the start command, or get it from the REST API.

With the REST API, set frameAncestors or corsOrigins in the create request. When the session is in-progress, read previewApiUrl and webPreviewToken from remoteConfig.

Embed the preview in an iframe

Start the session with --frame-ancestor set to the origin of your page. Then load the preview host URL, with its token query parameter, in an iframe:

<iframe src="https://<preview-host>/?token=<preview-token>" title="Expo Simulators preview" referrerpolicy="no-referrer" allow="clipboard-read; clipboard-write; fullscreen" style="width: 100%; height: 80vh; border: 0"></iframe>

On the first load, the preview host exchanges the token in the URL for a cookie.

Other sites cannot embed the private preview page on expo.dev. Use the preview host URL, not webPreviewUrl or the expo.dev link.

Render the screen with @expo/hub-client

@expo/hub-client provides React components that stream the device screen to your page and send touch and keyboard input back to the device. On iOS, they also send scrolling from a mouse wheel or a trackpad. Use it when you want only the device screen, inside your own layout.

Start the session with --cors-origin set to the origin of your page:

Terminal
- eas simulator:start --platform ios --cors-origin https://tools.example.com --max-duration-minutes 30 --json

Install the package in your web app:

Terminal
- npm install @expo/hub-client
- yarn add @expo/hub-client
- pnpm add @expo/hub-client
- bun add @expo/hub-client

Give DeviceClientProvider the preview host URL without the token query parameter, and give it the token separately:

import { DeviceClientProvider, DeviceScreen, displayScreen, useDeviceScreenClient, type DevicePlatform, } from '@expo/hub-client'; function Screen() { const client = useDeviceScreenClient(); const screen = displayScreen(client.screen); return ( <div style={{ position: 'relative', width: 390, aspectRatio: screen ? `${screen.width} / ${screen.height}` : '9 / 19.5', }}> <DeviceScreen client={client} /> </div> ); } export function SimulatorScreen({ previewHostUrl, token, platform, }: { previewHostUrl: string; token: string; platform: DevicePlatform; }) { const baseUrl = platform === 'android' ? `${previewHostUrl.replace(/\/+$/, '')}/vendor/serve-emu` : previewHostUrl; return ( <DeviceClientProvider platform={platform} options={{ baseUrl, token, streamMode: 'webrtc' }}> <Screen /> </DeviceClientProvider> ); }
  • client.status reports the connection state. If it stays connecting, check the URL, the token, and the --cors-origin value. A status of error means that the video stream failed, and client.error has the message.
  • DeviceScreen sends keyboard input only when it has focus. Click the screen or press Tab to focus it. The click also taps the device.
  • For an Android session, add /vendor/serve-emu to the preview host URL, as in the example above.

@expo/hub-client uses DOM elements, so it runs only on the web.

Use both methods in one session

You can pass --frame-ancestor and --cors-origin in the same command. Each flag controls a different browser check, so set the flag that matches how your page uses the session. An iframe needs only --frame-ancestor. A page that uses @expo/hub-client needs only --cors-origin.