serve-sim with React Native + Expo SDK 56
A complete, tested setup for streaming and controlling an iOS Simulator in the browser—standalone or mounted directly inside Expo’s Metro server.
Use an Expo development build, install serve-sim locally, mount its middleware at /.sim, and keep the server bound to localhost. Start Metro, boot the app, run npx serve-sim --detach, then open http://localhost:8081/.sim.
Overview
serve-sim is “the npx serve of Apple Simulators.” It captures a booted Simulator with a small native helper, streams it into a browser UI, forwards input over WebSockets, exposes logs and accessibility data, and provides CLI commands for repeatable interaction. It does not require app instrumentation and is not specific to React Native.
Two useful modes
| Mode | Start command | URL | Best for |
|---|---|---|---|
| Standalone | npx serve-sim | http://localhost:3200 | Fastest setup; no Metro customization |
| Expo embedded | npx expo start --dev-client + detached helper | http://localhost:8081/.sim | One project-local URL, agent/browser tooling, team convention |
The embedded UI still talks to a per-device helper. Starting serve-sim --detach explicitly makes the workflow deterministic.
What this guide assumes
- An Expo SDK 56 app running on the iOS Simulator.
- A local development build rather than relying on App Store Expo Go.
- npm commands; equivalents work with pnpm, Yarn, or Bun.
- Localhost-only access unless you deliberately secure remote access.
Requirements
- Host
- macOS on Apple Silicon (arm64). The bundled helper does not run on Intel Macs.
- Xcode
- Xcode 26.4 or newer for Expo SDK 56 native iOS builds.
- Node.js
- Node 20.19.4 or newer. React Native 0.85 is the limiting requirement, even though serve-sim itself accepts Node 18+.
- iOS target
- Expo SDK 56 raises the minimum iOS deployment target to 16.4.
- Camera injection
- Requires macOS 14+ and is intended for Simulator testing only.
Preflight
node --version
uname -m
xcodebuild -version
xcode-select -p
xcrun simctl list devices available
You want arm64, Node at least v20.19.4, a working Xcode developer directory, and at least one installed iOS Simulator runtime.
Expo says SDK 56’s Expo Go is not currently available through the Apple App Store. For a production app workflow, install expo-dev-client and build locally with npx expo run:ios.
Install
A. Existing SDK 56 project
cd your-expo-app
npm install --save-dev serve-sim@latest connect
npx expo install expo-dev-client
npx expo-doctor@latest
connect is used to compose the serve-sim middleware with Metro’s existing middleware. Keep serve-sim in the project so the team and agents use a known version.
B. New SDK 56 project
npx create-expo-app@latest my-app
cd my-app
npx expo install expo-dev-client
npm install --save-dev serve-sim@latest connect
npx expo-doctor@latest
C. Upgrade an SDK 55 app first
npx expo install expo@^56.0.0 --fix
npx expo-doctor@latest
npx expo install expo-dev-client
- If you use Continuous Native Generation, remove generated
ios/andandroid/directories from the previous SDK and regenerate them on the next build. - If you maintain native projects manually, do not delete them; apply the native upgrade changes and run
npx pod-install. - Build a new development client after upgrading SDK or changing native dependencies.
Metro integration
Expo’s supported customization path is a project-root metro.config.js that extends expo/metro-config. Generate it:
npx expo customize metro.config.js
Replace its contents with the following. This version preserves any pre-existing enhanceMiddleware hook instead of overwriting it.
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const connect = require('connect');
const { simMiddleware } = require('serve-sim/middleware');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
config.server = config.server || {};
const originalEnhanceMiddleware = config.server.enhanceMiddleware;
config.server.enhanceMiddleware = (metroMiddleware, server) => {
const middleware = originalEnhanceMiddleware
? originalEnhanceMiddleware(metroMiddleware, server)
: metroMiddleware;
const app = connect();
app.use(simMiddleware({ basePath: '/.sim' }));
app.use(middleware);
return app;
};
module.exports = config;
Why preserve the original middleware?
Expo or another package may already have registered middleware behavior. Calling the original hook first avoids silently breaking it. The serve-sim UI is then mounted before Metro at /.sim.
Quick config check
node -e "const c=require('./metro.config'); console.log(typeof c.server.enhanceMiddleware)"
Expected output: function. Restart Metro after changing the config.
Daily workflow
First native build—or after native dependency changes
open -a Simulator
npx expo run:ios
This generates/builds the native project as needed, installs the development app in Simulator, and starts Metro. In another terminal:
npx serve-sim --detach
open http://localhost:8081/.sim
Normal JavaScript/TypeScript iteration
Terminal 1:
npx expo start --dev-client
Terminal 2:
npx serve-sim --detach
open http://localhost:8081/.sim
If the app is not already open, press i in Expo CLI or launch it from the Simulator home screen.
Target a specific simulator
xcrun simctl list devices booted
npx serve-sim --detach "iPhone 17 Pro"
Names vary with installed runtimes. A UDID is more reliable when duplicate device names exist:
npx serve-sim --detach 00000000-0000-0000-0000-000000000000
Suggested package scripts
{
"scripts": {
"dev": "expo start --dev-client",
"ios": "expo run:ios",
"sim": "serve-sim",
"sim:bg": "serve-sim --detach",
"sim:list": "serve-sim --list",
"sim:stop": "serve-sim --kill"
}
}
With those scripts, the daily loop is npm run dev, npm run sim:bg, then open /.sim.
Standalone fallback
npx serve-sim
# Browser UI: http://localhost:3200
If standalone works but /.sim does not, the simulator helper is healthy and the issue is confined to Metro configuration or restart state.
Controls & automation
Browser controls
- Click or drag on the device surface for taps and swipes.
- Use the keyboard in focused fields; shortcuts including
⌘⇧Hare forwarded. - Hold Option while gesturing for pinch/zoom behavior.
- Drag images or videos into the preview to add them to the Simulator.
- Use device panels for rotation, home, appearance, permissions, logs, and accessibility inspection.
- Multiple booted simulators are supported.
Repeatable CLI commands
# Tap at normalized x/y coordinates
npx serve-sim tap 0.5 0.5
# Type into the focused field (US keyboard layout)
npx serve-sim type "hello@example.com"
printf 'long text' | npx serve-sim type --stdin
# Hardware and orientation
npx serve-sim button home
npx serve-sim rotate landscape_left
npx serve-sim memory-warning
# Appearance and accessibility
npx serve-sim ui appearance dark
npx serve-sim ui text-size extra-extra-extra-large
npx serve-sim ui reduce-motion on
npx serve-sim ui voiceover on
npx serve-sim ui status --json
Add -d <udid-or-name> to target one simulator when several are booted.
Gesture API
npx serve-sim gesture '{"type":"begin","x":0.5,"y":0.8}'
npx serve-sim gesture '{"type":"move","x":0.5,"y":0.2}'
npx serve-sim gesture '{"type":"end","x":0.5,"y":0.2}'
Coordinates are normalized from 0 to 1, which makes scripts portable across different device pixel sizes.
AI-agent workflows
The browser view is useful to agents because it combines a live device stream with interaction, logs, accessibility state, and deterministic CLI controls.
Install the serve-sim agent skill
# Open Agent Skills-compatible hosts (command from serve-sim README)
bunx add-skill EvanBacon/serve-sim
# Claude Code plugin marketplace
/plugin marketplace add EvanBacon/serve-sim
Bun is not required to run serve-sim; it is only used by the skill-install command above. Expo also ships official agent skills:
npx skills add expo/skills
Claude Code Desktop launcher
Create .claude/launch.json:
{
"version": "0.0.1",
"configurations": [
{
"name": "Apple",
"runtimeExecutable": "npx",
"runtimeArgs": ["serve-sim"],
"port": 3200
}
]
}
Reusable agent prompt
Run the Expo SDK 56 app in the iOS Simulator and use serve-sim to verify the requested flow.
1. Start or reuse Metro with `npx expo start --dev-client`.
2. Start the simulator helper with `npx serve-sim --detach`.
3. Use http://localhost:8081/.sim for visual inspection.
4. Prefer accessibility labels and normalized CLI coordinates for repeatable actions.
5. Capture exact errors and relevant simulator logs.
6. Do not enter real credentials, payment data, private tokens, or personal production data.
7. Do not expose serve-sim beyond localhost or change network binding without asking.
8. Stop helpers with `npx serve-sim --kill` when finished.
Advanced testing
Permissions
npx serve-sim permissions list com.example.app
npx serve-sim permissions grant camera com.example.app
npx serve-sim permissions grant location com.example.app --value always
npx serve-sim permissions revoke microphone com.example.app
npx serve-sim permissions reset all com.example.app
Supported permission names include notifications, location, camera, microphone, photos, contacts, calendar, reminders, motion, media-library, Siri, speech, Face ID, tracking, and HomeKit.
Camera injection
# Find your bundle identifier in app.json/app.config.* first
npx serve-sim camera com.example.app
npx serve-sim camera com.example.app --file ./fixtures/qr-code.png
npx serve-sim camera com.example.app --file ./fixtures/scan-loop.mp4
npx serve-sim camera --list-webcams
npx serve-sim camera com.example.app --webcam "MacBook Pro Camera"
# Hot-swap without relaunching the app
npx serve-sim camera switch placeholder
npx serve-sim camera switch ./fixtures/scan-loop.mp4
npx serve-sim camera mirror auto
npx serve-sim camera --stop-webcam
Use synthetic assets for QR scanners, identity-document framing, upload flows, and video pipelines. Never put production secrets or real customer documents in reusable fixtures.
Multi-device testing
npx serve-sim "iPhone 17 Pro" "iPad Pro 13-inch"
npx serve-sim --list
npx serve-sim --kill "iPad Pro 13-inch"
When names collide, use UDIDs. Remember that each device consumes CPU and memory for Simulator plus its streaming helper.
Networking & security
The current CLI warns that the preview includes a token-gated shell-exec route. The default bind address is 127.0.0.1. Do not casually publish the port or place it on an untrusted LAN.
Trusted-LAN access
npx serve-sim --host 0.0.0.0
Only do this on a trusted network and with macOS firewall controls understood. Anyone who can reach the UI may be able to control the simulator, inspect test content, or reach sensitive development capabilities.
Remote access
- Prefer a private VPN or an authenticated TLS tunnel with explicit access control.
- Do not expose raw helper ports directly to the public internet.
- Do not assume Expo’s own tunnel automatically secures or forwards serve-sim helper traffic.
- For custom single-port hosting, use
proxyHelpers: trueand forward HTTP upgrade events tomiddleware.handleUpgrade; otherwise video may load while input and DevTools fail. - Forward
X-Forwarded-Protothrough TLS proxies so generated URLs usehttps/wssrather than mixed-content-blockedhttp/ws.
Safe test data
Treat the browser preview like a remote-control surface. Use dedicated test accounts, synthetic media, non-production backends, and scrubbed logs. Stop helpers after use:
npx serve-sim --kill
Troubleshooting
/.sim returns 404
- Confirm
metro.config.jsis in the project root. - Confirm
require('serve-sim/middleware')resolves. - Stop and restart Metro; config changes are not hot-loaded.
- Check the actual Metro port printed by Expo. If Expo chose 8082, open
http://localhost:8082/.sim.
The UI loads but shows no device
open -a Simulator
xcrun simctl list devices booted
npx serve-sim --detach
npx serve-sim --list
A null response from /.sim/api means the middleware is mounted but no healthy helper state was selected.
Stale or frozen stream
npx serve-sim --kill
npx serve-sim --detach
Also shut down stale Simulators and restart the target device if its backing runtime changed.
Preview loads but taps or DevTools do not work
This usually means the WebSocket path is blocked. Check browser console errors, local firewall rules, reverse-proxy upgrade handling, and ws versus wss mixed-content failures.
serve-sim CLI not found in PATH from the device grid
Start the helper yourself with npx serve-sim --detach. If a host process cannot resolve the project-local binary, a global install is the fallback:
npm install --global serve-sim
xcrun simctl fails or hangs
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
xcrun simctl list devices available
Open Xcode once, install the required Simulator runtime, accept licenses, and verify command-line tools point at the full Xcode app rather than a stale installation.
Native build breaks after upgrading to SDK 56
- Verify Xcode 26.4+, Node 20.19.4+, and iOS target 16.4+.
- Run
npx expo install --fixandnpx expo-doctor@latest. - Regenerate native directories only if they are CNG-generated.
- Rebuild the development client after changing SDK/native dependencies.
Intel Mac
The packaged native helper is arm64-only. There is no configuration fix; use an Apple Silicon Mac or a remote Apple Silicon host.
Verification checklist
- Dependencies:
npm ls expo react-native serve-sim connect --depth=0 - Project health:
npx expo-doctor@latest - Metro:
curl -sS http://localhost:8081/status→packager-status:running - Embedded UI:
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8081/.sim→200 - Helper:
npx serve-sim --listshows the target device after startup. - Interaction: tap a harmless control, type into a test field, rotate, then return to portrait.
- Reload: edit a visible string and confirm Fast Refresh reaches the streamed app.
- Cleanup:
npx serve-sim --kill
A clean Expo SDK 56 project was created with Expo 56.0.12, React Native 0.85.3, React 19.2.3, serve-sim 0.1.43, and connect 3.7.0. The Metro config loaded, /.sim returned HTTP 200 with the serve-sim preview, Metro reported packager-status:running, and Expo Doctor passed 21/21 checks.
Sources
- serve-sim repository and README
- serve-sim on npm
- Expo SDK 56 release notes
- Expo: customizing Metro
- Expo: iOS Simulator setup
- Expo development builds
- Expo SDK upgrade walkthrough
Versions and behavior were checked on June 23, 2026. serve-sim is moving quickly; re-run npx serve-sim --help after upgrades.