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.

June 23, 2026·Prepared by Hermes·macOS / Apple Silicon
Recommended setup

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.

Expo
56.0.12
tested clean install
React Native
0.85.3
SDK 56 template
serve-sim
0.1.43
npm latest tested
Health checks
21 / 21
Expo Doctor passed

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

ModeStart commandURLBest for
Standalonenpx serve-simhttp://localhost:3200Fastest setup; no Metro customization
Expo embeddednpx expo start --dev-client + detached helperhttp://localhost:8081/.simOne 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 Go caveat on SDK 56

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/ and android/ 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 ⌘⇧H are 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

!
Keep it on localhost by default

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: true and forward HTTP upgrade events to middleware.handleUpgrade; otherwise video may load while input and DevTools fail.
  • Forward X-Forwarded-Proto through TLS proxies so generated URLs use https/wss rather than mixed-content-blocked http/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

  1. Confirm metro.config.js is in the project root.
  2. Confirm require('serve-sim/middleware') resolves.
  3. Stop and restart Metro; config changes are not hot-loaded.
  4. 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 --fix and npx 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

  1. Dependencies: npm ls expo react-native serve-sim connect --depth=0
  2. Project health: npx expo-doctor@latest
  3. Metro: curl -sS http://localhost:8081/statuspackager-status:running
  4. Embedded UI: curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8081/.sim200
  5. Helper: npx serve-sim --list shows the target device after startup.
  6. Interaction: tap a harmless control, type into a test field, rotate, then return to portrait.
  7. Reload: edit a visible string and confirm Fast Refresh reaches the streamed app.
  8. Cleanup: npx serve-sim --kill
Guide validation performed

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

Versions and behavior were checked on June 23, 2026. serve-sim is moving quickly; re-run npx serve-sim --help after upgrades.

Columbia Pages · generated by HermesTechnical guide · verified against Expo SDK 56