expo-device-hub
Expo Device Hub is an Expo DevTools plugin
that lets you preview and control your iOS simulators and Android emulators right from
the browser — without leaving your development workflow. When you run expo start, the
Hub adds a device dashboard where you can watch a live stream of any device, interact
with it, and manage which devices are running from one place.
Features
- Live stream of iOS simulators and Android emulators in your browser.
- Interact directly — tap, swipe, scroll, and type into the device.
- Boot, shut down, and add devices without opening Xcode or Android Studio.
- Follows your system light/dark theme, and can flip the device's appearance too.
- Feed an Android emulator's camera a PNG from the inspector's Camera section.
iOS simulators require macOS with Xcode. Android emulators require the Android SDK (
emulator,adb).
Use in an Expo app
Using the Hub inside an Expo app requires Expo SDK 57 or newer.
Install the plugin:
npx expo install expo-device-hubThen start your project as usual:
npx expo startExpo Device Hub registers itself as a DevTools plugin, so a link to it appears in your terminal when the dev server starts:
› Expo Device Hub: http://localhost:8081/_expo/plugins/expo-device-hub
Use standalone
The Hub also runs outside of expo start as a standalone server — useful when you want
the device dashboard without a running Expo project:
npx expo-device-hubScreenshots in EAS sessions
Set EXPO_DEVICE_HUB_SCREENSHOT_DIRECTORY to save each preview screenshot as a file.
The browser download still works as before.
Both serve-sim and serve-emu write each captured PNG to a temporary file in that
directory, then rename it.
A saved file has a name such as screenshot-2026-09-24T08-45-59-123Z-a1b2c3d4e5f6.png.
The name contains the UTC capture time and a random suffix.
If the save fails, serve-sim or serve-emu logs the error to stderr and writes a screenshot-<time>-<suffix>.failed.json record with the file name, error, and time into the same directory. The EAS worker reports that record.
The screenshot response still returns the PNG.
Each screenshot response carries the save outcome in the X-Expo-Screenshot-Artifact header.
When the save fails, the serve-sim preview and the Device Hub dashboard show a notice after the download.
On iOS, serve-sim records each manual screenshot as a session event in the Events panel.
A failed save appears there as an error event with the reason.
The reason sent to the browser is only the error code, such as ENOSPC, or unknown error. The full error, with the path, goes only to the stderr line and the .failed.json record. The EAS worker reports that record in the job log.
A serve-sim preview opened on a loopback hostname keeps its Desktop save and does not use the directory.
Standalone previews without the environment variable keep their existing behavior.
The EAS worker in eas-cli build-tools sets the variable and uploads completed files.
Upload naming and retry behavior are documented in
deviceRunSessionScreenshots.ts.
Repository structure
This is a Bun workspace orchestrated with Turborepo.
| Package | What it is |
|---|---|
packages/expo-device-hub | The main DevTools plugin. |
packages/@expo/hub-client | Device-client hooks and types that own the connection to serve-sim / serve-emu and paint the live stream. See hub-client below. |
packages/@expo/hub-components | Dependency-free UI kit (Sidebar, StreamPanel, Button, …) built on @expo/styleguide design tokens, so Hub matches the Expo dashboard website. |
packages/@expo/hub-apple-utils | Lists, creates, and boots Apple simulator devices via simctl (macOS only). |
packages/@expo/hub-android-utils | Lists, creates, and boots Android emulators via avdmanager / sdkmanager / emulator. |
packages/serve-sim | Source for @expo/serve-sim, maintained and published from this monorepo. |
packages/serve-emu | Source for the serve-emu workspace package, maintained in this monorepo. |
example | A minimal Expo app with the plugin installed. |
Getting started
Install dependencies with Bun 1.3.14 and build every package once from the repo root:
bun install
bun run build # turbo build across all packagesFormatting
JavaScript and TypeScript use Oxfmt with double quotes, including JSX attributes,
as configured in .oxfmtrc.json. Oxfmt may choose the other quote when it avoids
escaping. bun run format formats all supported repository files, and
bun run lint checks their formatting before running the workspace linters.
The existing EAS Checks job runs the same lint command. Installed agent skills
and LLP tooling are excluded so their upstream copies and receipts stay intact.
Run the example
The example app is a host Expo project that has expo-device-hub
installed as a DevTools plugin. Use it to see the Hub exactly as an end user would.
cd example
bun start # or: bun run ios / bun run android / bun run webDevelop
To iterate on the dashboard UI with Metro fast refresh, run expo-device-hub as a
standalone Expo web app:
cd packages/expo-device-hub
bun start # expo start --web, on port 8081This serves the Dashboard component directly, so edits to the UI hot-reload without going through a host app.
A local "inception" DevTools module (modules/expo-device-hub)
registers the plugin against itself, so the standalone app still gets the real
/api/devices backend while you develop.
The device server is bundled (see
scripts/build-plugin-server.ts), so hot reload covers the UI. After changing anything undersrc/server, rebuild it withbun run build:server.
CI and Xcode support
The published packages build serve-sim's native code with Xcode 26, so they work for users on Xcode 26 and Xcode 27. LLP 0014 has the full support statement.
| When | What runs |
|---|---|
| Every push to a PR | Checks: lint and typecheck |
| Every push to a PR that is ready for review | serve-sim tests: the Xcode 26 build on the iOS 26 and iOS 27 Simulators |
Every push to a draft PR with the ci-ready label | serve-sim tests, as above |
Every push to a PR with the xcode-27 label | Also an Xcode 27 build, tested on the iOS 27 Simulator |
Every night on main | The Xcode 27 build, tested on the iOS 27 Simulator |
The serve-sim tests run only when a PR changes serve-sim or its CI files, and they use
several macOS machines. Open a PR as a draft while you iterate, and add ci-ready
only when you want a run.
hub-client
@expo/hub-client is the device-client layer. The
two backends speak very different wire protocols — serve-sim streams MJPEG/H.264 and
takes binary touch packets, while serve-emu streams H.264 (WebCodecs) and takes JSON
gestures — so this package hides that behind one shared contract:
DeviceClientProviderto share one connection, withuseDeviceClientanduseDeviceClientSelectorto read state and controls,deviceSettingsStatusto report device-options loading and availability independently of video and app activity,- a
DeviceScreencomponent that paints whichever stream is active and forwards pointer/gesture/keyboard/scroll-wheel input, and - a
KeyboardCapturecomponent (withuseCoarsePointer) that lets touch clients type into the device with their phone keyboard viaclient.sendKeyEvents.
The serve-sim side tracks the @expo/serve-sim web client: host work
(simulator settings, app-bundle details) goes through its typed exec-ws actions rather
than shell commands. The client is written against the server in
packages/serve-sim, in this monorepo.
It lives in its own package (rather than inside the plugin) so the Expo dashboard
website can consume the exact same code to mirror devices in the browser. It is published
to npm as @expo/hub-client; see the
package README for install and usage examples.