> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revyl.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Expo

> Build an Expo app for iOS and Android with EAS and run it in the dev loop with hot reload.

Create dev and preview builds for your Expo React Native app.

Prompt for your coding agent:

```text
Set up Revyl for this Expo app.

Read https://docs.revyl.com/frameworks/expo.md and follow it. It's the current Revyl guide for this, so prefer it over anything you remember about Revyl.

`revyl init` writes the dev loop's `development` recipe with EAS, which needs my Expo login or `EXPO_TOKEN`, so ask me before using it. If this project doesn't already build with EAS, install `expo-dev-client` and change the `development` recipes to `expo prebuild` plus a Debug simulator build (`xcodebuild -configuration Debug -sdk iphonesimulator`) and `./gradlew assembleDebug`. If the app runs on both platforms, start with iOS.

You're done when the app runs in the Revyl dev loop on a cloud simulator or emulator, and a JavaScript change hot reloads.

Ask me before anything that needs my login or approval.
```

## Build Guidance

To use the [Dev Loop](/dev-loop), a build is required **only on native code changes** (new native modules, Podfile/Gradle changes, build configuration). JS/TS changes hot reload via Metro — see [Dev Loop: Hot reload and rebuild configuration](/frameworks#hot-reload-and-rebuild-configuration) for the full breakdown.

## Build via Revyl CLI

### Prerequisites

- Node.js 18+
- Expo CLI (`npx expo`)
- EAS CLI (`npx eas-cli`) only when your authored recipe uses EAS
- Revyl CLI installed and authenticated (`revyl auth login`)

### Setup

Initialize the project using the Revyl CLI:

```bash
revyl init
```

This detects Expo and writes named profiles with the applicable iOS and Android
recipes. Review or fill in the app IDs, commands, and output paths before the
first build. See [Types of Builds](/builds#types-of-builds) for more information.

### Choose a build approach

| Approach                                     | Recipe                                             | Expo authentication                                                                               |
| -------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Native prebuild (default in dashboard setup) | Run `expo prebuild`, then Xcode or Gradle directly | None required solely for Expo                                                                     |
| EAS local build                              | Run `eas-cli build --local`                        | Use Expo login or `EXPO_TOKEN` locally; store encrypted `EXPO_TOKEN` for unattended remote builds |

Choosing the Expo framework does not itself require `EXPO_TOKEN`. Add that
secret only when the selected build command uses EAS or another project step
needs it. The dashboard leaves commands and secret references directly
editable; create any required secret inline or in the Library.

Development builds power the dev loop. Preview builds are for testing and starting sessions when the Metro server isn't needed.

### Run Build

Run one profile/platform recipe in the cloud:

```bash
revyl build --profile development --platform ios
```

Profile names and their `ios` or `android` recipes live under
`build.profiles` in `.revyl/config.yaml`. There is no active/default profile.
See [Configuration](/remote-builds/configuration) for selection and recipe
fields.

This command automatically builds and uploads the app to Revyl, where you can use it to start sessions.

## Advanced Configuration

When you run `revyl init`, Revyl makes a best-effort guess at your configuration. This section covers configuration in more detail.

### EAS setup

This section applies only when an authored recipe uses EAS. Your `eas.json`
needs at least two profiles: one that includes the development client and one
that does not. See the following `eas.json` file as reference:

```json
{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "simulator": true
      },
      "android": {
        "buildType": "apk"
      }
    },
    "preview": {
      "distribution": "internal",
      "ios": {
        "simulator": true
      },
      "android": {
        "buildType": "apk"
      }
    }
  }
}
```

Note that the iOS simulator is enabled for both profiles, and `developmentClient` is set to `true` only for the development profile.
Revyl executes the command arrays in `.revyl/config.yaml` as authored; it does
not rewrite `eas.json` or switch EAS profiles during a build.

### Build Commands and Hot Reload Setup

`revyl init` creates a `.revyl/config.yaml` file in the root of your project.
Each named profile contains the ordered setup/build commands and output path
used by `revyl build`.

Configure these build settings using the [Configuration](/reference/cli) page.

Expo dev-client hot reload needs a URL scheme baked into the native dev build. This is normally set as part of `revyl init`. The scheme is the custom URL prefix for your app, such as `myapp-dev://`. Revyl uses that scheme to open the Expo dev client on the cloud device and connect it to Metro.

If you use `app.json`, set `expo.scheme` before creating the dev build:

```json
{
  "expo": {
    "scheme": "myapp-dev"
  }
}
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="Build fails or times out">
    - Stream detailed diagnostics: `revyl build --profile development --platform ios --debug`
    - For an unattended remote EAS recipe, verify that `EXPO_TOKEN` is stored and referenced
    - Check EAS build credits only when the recipe uses EAS
    - Verify `package.json` dependencies
    - For EAS cloud builds, review the EAS dashboard logs
    - For EAS `--local` builds, review command output and logs in the local EAS working directory
    - Validate `eas.json`
  </Accordion>

  <Accordion title="Download links expired">
    - Artifacts expire after some time
    - Re-run build for fresh links
    - Download immediately after build
  </Accordion>

  <Accordion title="iOS or Android APK app won't install">
    - For iOS, ensure `simulator: true` is set in `eas.json`. For Android, ensure the profile builds an `apk`, not an `aab`.
  </Accordion>
</AccordionGroup>

## Dev loop

Expo projects use full hot reload via a Revyl relay to your local Metro server. JS/TS changes appear on the device within seconds. Press `[r]` only when native dependencies change.

#### How detection works

The CLI looks for two things in the current directory:

1. `"expo"` in `package.json` dependencies or devDependencies
2. At least one project indicator: `app.json`, `app.config.js`, `app.config.ts`, `eas.json`, or `.expo/` directory

If both are present, the Expo provider matches at **confidence 0.9** — the highest of any provider. This means Expo wins over Swift (0.7) and Android (0.6) when all three detect the same directory.

If either condition fails (common in monorepos), the Expo provider returns nil and lower-confidence providers may match instead. See the monorepo section below.

#### URL schemes

Hot reload deep-links the dev client to your local Metro server via a custom URL scheme:

```
myapp://expo-development-client/?url=https://hr-abc123.revyl.ai
```

This requires a **custom URL scheme** (`myapp://`) registered in the dev client binary.

##### Where to find your scheme

Check `app.json` under `expo.scheme`:

```json
{
  "expo": {
    "scheme": "myapp"
  }
}
```

Or in `app.config.js` / `app.config.ts`:

```javascript
export default {
  expo: {
    scheme: "myapp",
  }
};
```

Keep this value in the Expo app configuration. It is baked into the development
client and attached to the resulting build metadata; it is not an authored
`.revyl/config.yaml` field.

##### Why universal links don't work

Apps that only use universal links (`https://example.com/...`) for deep linking cannot use those for hot reload. Apple's associated domains system requires a live HTTPS domain serving an `apple-app-site-association` file. Revyl hot reload uses short-lived relay hosts that are not suitable as stable associated domains. Custom URL schemes (`myapp://`) bypass this entirely — no server verification needed.

##### No URL scheme in the app

If your app has no `expo.scheme` (common in apps that only use universal links), you need to add one. The name is arbitrary and only affects the dev client:

```json
{
  "expo": {
    "scheme": "myapp-dev"
  }
}
```

After adding the scheme, **rebuild the dev client** — the scheme is baked into
the native binary and recorded with the build at build time. If initialization
also generated the wrong framework or recipe, run `revyl init --detect` from
the app directory, edit the generated profile as needed, and run
`revyl config validate`.

##### Checking existing schemes without rebuilding

The dev client may already have a scheme registered by `expo-dev-client`. Check the iOS build:

```bash
grep -r "CFBundleURLSchemes" ios/ --include="*.plist" -A3
```

If this returns something like `exp+myslug`, that scheme is already baked into
the client. Build it with `revyl build` so the artifact carries the same scheme
metadata.

##### Generated scheme prefixes

Expo dev clients can register URL schemes in two formats:

- **Base scheme**: `myapp://` — registered when `expo.scheme` is set in app.json
- **Prefixed scheme**: `exp+myapp://` — auto-registered by `expo-dev-client` based on the app slug

Which form exists depends on the Expo SDK and the `addGeneratedScheme` setting
at build time. If the selected build lacks scheme metadata or the registered
scheme is wrong, configure `expo.scheme` (and `addGeneratedScheme` when
applicable) in the Expo app and rebuild the development client.

#### Monorepo setup

In monorepos (Turborepo, Nx, pnpm workspaces), the Expo app typically lives in a subdirectory like `apps/native/`, `apps/mobile/`, or `packages/app/`.

##### Run from the Expo app directory

All Revyl commands must run from the directory containing the Expo app's `package.json` — **not the monorepo root**.

```bash
cd apps/native
revyl init --detect
revyl dev --platform ios
```

The CLI resolves the working directory by calling `FindRepoRoot`, which walks up from the current directory looking for `.revyl/`. If the monorepo root also has a `.revyl/` directory (common for CI test configs), make sure you're in the correct subdirectory.

##### Why detection fails in monorepos

Two things go wrong:

1. **Hoisted dependencies** — Package managers like pnpm, Yarn, and npm may hoist `expo` to the root `node_modules/` or use `workspace:*` protocol in the local `package.json`. The Expo provider reads the local `package.json` and checks for `"expo"` in `dependencies` or `devDependencies`. If it's not there, the provider returns nil (no match).

2. **Native directories match other providers** — Every Expo project with prebuild has `ios/` containing `.xcodeproj` files (triggering the Swift provider at confidence 0.7) and `android/` containing `build.gradle` (triggering the Android provider at confidence 0.6). When the Expo provider returns nil, these become the top matches.

The result: `revyl init` detects "Swift/iOS (coming soon)" and "Android (coming soon)" instead of Expo.

**Fix**: Rerun detection from the Expo app directory:

```bash
cd apps/native
revyl init --detect
```

If detection remains ambiguous, edit `build.framework` and the generated
profile recipes in `.revyl/config.yaml`, then run `revyl config validate`. If
`expo` is genuinely missing from the local `package.json`, add it:

```bash
npx expo install expo
```

The generated `.revyl/config.yaml` should have `build.framework: expo` and a
named development profile with the applicable iOS and/or Android recipes. The
custom URL scheme remains in the Expo app configuration, not the Revyl YAML.

#### What to expect when the deep link opens

After `revyl dev` starts, the CLI opens a deep link to connect the dev client to your local Metro server. You may notice:

1. **A confirmation dialog** -- iOS shows "Open in [Your App]?" with Cancel and Open buttons. This is a standard iOS security prompt for URL scheme handoffs. Tap **Open** to proceed. (On cloud simulators this currently requires a manual tap; future CLI versions will auto-accept it.)

2. **The app briefly restarts** -- The dev client may close and reopen as it disconnects from its cached state and reconnects to the tunnel URL. This looks jarring but is normal Expo dev client behavior. The app is reloading to fetch the JS bundle from your local Metro server instead of its built-in bundle.

3. **Sometimes no restart at all** -- If the dev client is already in a fresh state (first launch after install), it may connect seamlessly without closing. The behavior varies by Expo SDK version and whether the app was previously running.

Once the app is back up and showing your app's UI, hot reload is active. Edit a file locally, save, and the change appears on the device within seconds.

If the app closes and **doesn't come back**, check the Metro/Expo terminal output for JavaScript errors. A crash during bundle loading usually means a missing native module or environment variable issue -- not a Revyl problem.

#### Dynamic config (app.config.js / app.config.ts)

Dynamic Expo config must still resolve a custom scheme. Verify the evaluated
configuration with Expo tooling. For managed builds, Revyl reads the registered
scheme from the compiled app and attaches it as artifact metadata. If inspection
is unavailable, the build and upload still succeed, but that artifact cannot
start an Expo dev loop until compatible scheme metadata is available.

The scheme is the custom URL prefix baked into the Expo dev client, such as
`myapp-dev://`. Revyl uses it to open the installed dev client on the cloud
device and connect it to Metro for hot reload.

Configure the scheme in `app.config.js` or `app.config.ts`, then run detection
from the app directory:

```bash
revyl init --detect
```

If the generated framework or recipe is still wrong, edit the named profile in
`.revyl/config.yaml` and run `revyl config validate`.

If you add or change the scheme, rebuild the Expo dev client once. After that,
JS/TS changes can hot reload through `revyl dev` without another build.

#### Custom dev server port

If Metro runs on a non-default port (common in monorepos with multiple Metro
instances), pass it for the invocation:

```bash
revyl dev --platform ios --port 8082
```

## Dev loop walkthrough

You need the [prerequisites](#prerequisites) above and a dev client build of your Expo or React
Native project.

### Step 1: Configure hot reload

```bash
revyl init
```

Initialization detects the framework and writes named build profiles. In a
monorepo, run `revyl init --detect` from the Expo app directory. If detection
still needs correction, edit `build.framework` and the generated development
profile, then run `revyl config validate`.

Configure the custom URL scheme in the Expo app before building the development
client; the scheme is attached to the build artifact rather than authored in
`.revyl/config.yaml`. See [Configuration Files](/reference/cli/configuration) for the
profile schema and [Dev Loop Framework Guides](/frameworks/ios) for
framework-specific setup.

### Step 2: Upload a dev build

Upload a development client build. It lands on the Revyl branch named after your
Git branch, which the first upload creates, or on the app's default branch
outside a Git checkout:

```bash
revyl build --profile development --platform ios
```

For Expo, this is a dev client build. For bare React Native, it's your debug build.

`revyl build` runs in the cloud.
The dev loop remains local unless you use `revyl dev --remote`.

`revyl dev` resolves one named profile/platform recipe, runs on the branch named
after your Git branch, and installs that branch's default build. Pass
`--branch <name>` to both build and dev to use another branch. For every branch
and build rule, see [Dev Loop](/reference/cli#dev-loop).

### Step 3: Start the dev loop

```bash
revyl dev --profile development --platform ios
```

You may omit the flags when the result is deterministic. `revyl dev` first
prefers one unique development-like profile and then accepts only a sole
eligible profile or platform. It prompts for remaining choices interactively
and fails with the valid choices non-interactively. This is invocation-time
selection, not an active/default profile.

This:

1. Starts your local dev server (Expo via `npx expo start --dev-client`, or Metro via `npx react-native start`)
2. Creates a Revyl relay to expose it to cloud devices
3. Installs the dev client build on a cloud device
4. Opens the device session in your browser

Normal runs keep advisory HMR diagnostics quiet so they do not look like startup
failures. If you are troubleshooting relay or HMR internals, run with
`revyl dev --debug` to print the diagnostic probes.

Now edit code locally and see changes reflected on the device instantly.

#### Platform and build overrides

```bash
revyl dev --profile development --platform android  # Explicit recipe
revyl dev --no-open                       # Don't open browser (headless/SSH)
revyl dev --profile development --platform ios --build  # Fresh local build
revyl dev --profile development --platform ios --remote # Remote build
revyl dev --build-version-id <id>         # Use one exact build
revyl dev --context ios-main              # Named context for parallel loops
revyl dev --force-hot-reload              # Diagnostic launch after Expo relay transport
revyl dev --no-build --tunnel "<expo-dev-client-link>"  # Reuse an Expo tunnel
```

The selected recipe remains fixed for the dev context. Forced builds, remote
builds, and later `revyl dev rebuild` operations all use that same recipe.

If Revyl starts Expo and verifies relay transport but times out proving the
first Expo manifest, retry with `--force-hot-reload` first. This launches after
the relay and dev server start, skipping only the manifest and bundle proof. If
the app loads, keep working; if the dev client shows a project load error,
restart Expo/Metro or capture `revyl device report --session-id <id> --json`.

If you already run Expo with its own tunnel, you can collapse the manual device
start + deep-link step into one command:

```bash
npx expo start --tunnel --dev-client
revyl dev --no-build --app-id <app-id> --tunnel "<deep-link-from-expo>"
```

`--no-build` reuses an existing build and keeps rebuilds off for this session.
Add `--branch <name>` to require a specific branch's build.

`--tunnel` accepts either the full Expo dev-client link or the raw `https://...`
tunnel URL. Passing the full dev-client link does not depend on an authored
scheme field in the local Revyl config. Prefer the Revyl relay first in
cloud-agent environments; use Expo tunnel fallback only after device screenshots
or `revyl device report --session-id <id> --json` show force mode still did not
load through the relay.

### Step 4: Interact with the device

Use `revyl device` commands to observe, act, and verify in a tight loop.

```bash
# Observe
revyl device screenshot --out before.png

# Act
revyl device tap --target "Sign In button"
revyl device type --target "email field" --text "user@example.com"
revyl device type --target "password field" --text "secret123"
revyl device tap --target "Log In"

# Verify
revyl device screenshot --out after.png
```

Always follow the **observe-act-verify** pattern: screenshot before an action, take the action, then screenshot again to confirm the result.

Scroll through content with swipe:

```bash
revyl device swipe --target "product list" --direction down
```

### Step 5: Verify flows live

While the dev loop is active, verify outcomes with device commands:

```bash
revyl device validation -s 0 "Home screen shows the signed-in user"
```

### Step 6: Create tests from the session

After verifying a flow manually, convert the device session into a regression test:

```bash
revyl test create checkout-flow --from-session <session-id> --platform ios
revyl test open checkout-flow
```

### Step 7: Promote to regression

Once the test is stable, push and run it:

```bash
revyl test push checkout-flow --force
revyl test run checkout-flow
```

---

### Device-first flow

Start a plain device session first, then attach and run the dev loop on it:

```bash
revyl device start --platform ios
revyl dev attach active --context checkout
revyl dev --context checkout            # reuses the attached session
```

When the dev loop exits, the attached session stays running. Run `revyl dev --context checkout` again to resume, or `revyl dev stop` to detach.

### Context management

```bash
revyl dev list                         # List dev contexts in the current worktree
revyl dev use ios-main                 # Switch the current context
revyl dev status                       # Show context status (JSON)
revyl dev rebuild                      # Trigger a rebuild
revyl dev stop                         # Stop the current context
revyl dev stop --all                   # Stop all contexts
```


### Team sharing

All developers push builds to a shared app container (the `app_id` in config). Each developer gets their own cloud device session, relay URL, and local server. For JS projects, multiple developers can share the same dev build and still see their own code changes.

## Resources

- [EAS Build Docs](https://docs.expo.dev/build/introduction/)
- [EAS Configuration](https://docs.expo.dev/build/eas-json/)
- [Development Builds](https://docs.expo.dev/development/introduction/)
