> ## 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.

# Auth Bypass Deep Links

> Use launch variables and deep links to reach authenticated app states in test builds

Auth bypass deep links let Revyl start a test from a signed-in app state without automating every login screen.

The easiest implementation is:

1. Add a test-only deep link handler in your app.
2. Gate that handler with Revyl launch variables.
3. Open one deep link from `revyl device navigate` or a YAML `navigate` step.

<Warning>
  Only enable this in simulator, staging, debug, or explicit test builds. Do not
  ship a permanent auth bypass in production.
</Warning>

## Fastest Path For Expo Cloud Agents

Use this path when you already run an Expo development client through `revyl dev`.

```bash theme={null}
npx expo start --tunnel --dev-client

revyl dev --no-build --tunnel "<expo-dev-client-link>" \
  --launch-var REVYL_AUTH_BYPASS_ENABLED \
  --launch-var REVYL_AUTH_BYPASS_TOKEN

revyl device navigate \
  --url "myapp://revyl-auth?token=$REVYL_AUTH_BYPASS_TOKEN&role=buyer&redirect=%2Fcheckout"
```

Load the Expo dev client project first, then send the app-specific auth deep link. In managed Expo apps, JavaScript may not automatically see native launch values; use a small native config bridge, verify the token with a staging backend, or use a local demo fallback for sample apps only.

## Copy This Contract

Use one route shape across every framework:

```text theme={null}
myapp://revyl-auth?token=<token>&role=<role>&redirect=<allowlisted-route>
```

Create these launch variables:

```bash theme={null}
revyl global launch-var create REVYL_AUTH_BYPASS_ENABLED=true
revyl global launch-var create REVYL_AUTH_BYPASS_TOKEN=revyl-demo-token --secret
```

Then start a device with the variables attached and open the bypass link:

```bash theme={null}
revyl device start \
  --platform ios \
  --launch-var REVYL_AUTH_BYPASS_ENABLED \
  --launch-var REVYL_AUTH_BYPASS_TOKEN

revyl device navigate \
  --url "myapp://revyl-auth?token=revyl-demo-token&role=buyer&redirect=%2Fcheckout"
```

Use the same URL in YAML tests:

```yaml theme={null}
test:
  metadata:
    name: "Auth bypass to checkout"
    platform: ios

  build:
    name: "Shopping App"

  env_vars:
    - REVYL_AUTH_BYPASS_ENABLED
    - REVYL_AUTH_BYPASS_TOKEN

  variables:
    auth_bypass_token: revyl-demo-token

  blocks:
    - type: manual
      step_type: navigate
      step_description: "myapp://revyl-auth?token={{auth_bypass_token}}&role=buyer&redirect=%2Fcheckout"

    - type: validation
      step_description: "The checkout screen is visible and the user is signed in"
```

## App Checklist

Your app owns four small pieces:

* Read `REVYL_AUTH_BYPASS_ENABLED` and `REVYL_AUTH_BYPASS_TOKEN` at app launch.
* Register a handler for `myapp://revyl-auth`.
* Verify the token, role, and redirect before changing app state.
* Create a test-only session and route only to an allowlisted destination.

Invalid links should fail visibly in test builds. Show a debug banner, account-screen status, toast, or log line that says why the link was rejected.

## Framework Recipes

Pick the recipe closest to your app. The session creation and route names are app-specific, but the Revyl contract stays the same.

<AccordionGroup>
  <Accordion title="Expo / Expo Router">
    Put the handler near your root layout so it catches initial links and links opened while the app is running.

    ```tsx theme={null}
    import { useEffect } from "react";
    import * as Linking from "expo-linking";
    import { router } from "expo-router";

    const allowedRedirects = new Map([
      ["/account", "/(tabs)/account"],
      ["/cart", "/cart"],
      ["/checkout", "/checkout"],
    ]);
    const allowedRoles = new Set(["buyer", "support"]);

    function launchValue(key: string) {
      return process.env[key];
    }

    function handleRevylAuthBypass(rawURL: string) {
      const url = new URL(rawURL);
      if (url.protocol !== "myapp:" || url.hostname !== "revyl-auth") return;

      const enabled = launchValue("REVYL_AUTH_BYPASS_ENABLED") === "true";
      const expectedToken = launchValue("REVYL_AUTH_BYPASS_TOKEN");
      const token = url.searchParams.get("token");
      const role = url.searchParams.get("role") || "buyer";
      const redirect = url.searchParams.get("redirect") || "/account";
      const route = allowedRedirects.get(redirect);

      if (!enabled) throw new Error("Revyl auth bypass is disabled");
      if (!expectedToken || token !== expectedToken) throw new Error("Bad Revyl auth bypass token");
      if (!allowedRoles.has(role)) throw new Error("Role is not allowlisted");
      if (!route) throw new Error("Redirect is not allowlisted");

      createTestSession({ role });
      router.replace(route);
    }

    export function useRevylAuthBypass() {
      useEffect(() => {
        Linking.getInitialURL().then(url => {
          if (url) handleRevylAuthBypass(url);
        });

        const subscription = Linking.addEventListener("url", event => {
          handleRevylAuthBypass(event.url);
        });

        return () => subscription.remove();
      }, []);
    }
    ```

    In managed Expo apps, JavaScript may not receive native launch values automatically. Use a small native config bridge, verify the token with a staging backend, or follow the Bug Bazaar sample's demo fallback for a local fixture only.

    For Expo Router apps, add a route backstop such as `app/revyl-auth.tsx` that calls the same handler from route params. Expo Router may treat `myapp://revyl-auth?...` as a normal app route while the development client is already running; the route backstop keeps rejected links visible instead of landing on an unmatched-route screen.
  </Accordion>

  <Accordion title="React Native CLI">
    Use React Native `Linking` for the deep link and read launch config from your native layer or staging backend.

    ```tsx theme={null}
    import { useEffect } from "react";
    import { Linking, NativeModules } from "react-native";

    const allowedRedirects = new Set(["/account", "/checkout", "/cart"]);
    const allowedRoles = new Set(["buyer", "support"]);

    async function getLaunchConfig() {
      return NativeModules.LaunchConfig.getRevylAuthBypassConfig();
      // { enabled: boolean, token: string }
    }

    async function handleRevylAuthBypass(rawURL: string) {
      const url = new URL(rawURL);
      if (url.protocol !== "myapp:" || url.hostname !== "revyl-auth") return;

      const config = await getLaunchConfig();
      const token = url.searchParams.get("token");
      const role = url.searchParams.get("role") || "buyer";
      const redirect = url.searchParams.get("redirect") || "/account";

      if (!config.enabled) throw new Error("Revyl auth bypass is disabled");
      if (token !== config.token) throw new Error("Bad Revyl auth bypass token");
      if (!allowedRoles.has(role)) throw new Error("Role is not allowlisted");
      if (!allowedRedirects.has(redirect)) throw new Error("Redirect is not allowlisted");

      await createTestSession({ role });
      navigationRef.navigate(routeNameForRedirect(redirect));
    }

    export function useRevylAuthBypass() {
      useEffect(() => {
        Linking.getInitialURL().then(url => {
          if (url) void handleRevylAuthBypass(url);
        });

        const subscription = Linking.addEventListener("url", event => {
          void handleRevylAuthBypass(event.url);
        });

        return () => subscription.remove();
      }, []);
    }
    ```

    For iOS simulators and physical devices, expose environment-variable
    configurations from the compatible `-KEY value` pairs in
    `ProcessInfo.processInfo.arguments`. These pairs are distinct from explicit
    iOS argument sets, which are passed as raw ordered tokens. For Android,
    expose environment configurations from the launch `Intent` extras.
  </Accordion>

  <Accordion title="Native iOS">
    On iOS simulators and physical devices, Revyl encodes environment-variable
    configurations as compatible launch-argument pairs:
    `-REVYL_AUTH_BYPASS_ENABLED true -REVYL_AUTH_BYPASS_TOKEN <token>`.

    ```swift theme={null}
    import SwiftUI

    private let allowedRedirects: [String: AppRoute] = [
        "/account": .account,
        "/checkout": .checkout,
        "/cart": .cart
    ]
    private let allowedRoles: Set<String> = ["buyer", "support"]

    func launchValue(_ key: String) -> String? {
        let args = ProcessInfo.processInfo.arguments
        guard let index = args.firstIndex(of: "-\(key)") else { return nil }
        let valueIndex = args.index(after: index)
        return args.indices.contains(valueIndex) ? args[valueIndex] : nil
    }

    func queryValue(_ name: String, in url: URL) -> String? {
        let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
        return items.first(where: { $0.name == name })?.value
    }

    func handleRevylAuthBypass(_ url: URL) throws {
        guard url.scheme == "myapp", url.host == "revyl-auth" else { return }

        guard launchValue("REVYL_AUTH_BYPASS_ENABLED") == "true" else { throw AuthBypassError.disabled }
        guard queryValue("token", in: url) == launchValue("REVYL_AUTH_BYPASS_TOKEN") else { throw AuthBypassError.badToken }

        let role = queryValue("role", in: url) ?? "buyer"
        let redirect = queryValue("redirect", in: url) ?? "/account"
        guard allowedRoles.contains(role) else { throw AuthBypassError.badRole }
        guard let route = allowedRedirects[redirect] else { throw AuthBypassError.badRedirect }

        TestSession.signIn(role: role)
        AppRouter.shared.navigate(to: route)
    }
    ```

    Call `handleRevylAuthBypass(_:)` from your SwiftUI `.onOpenURL` handler or your app delegate URL handler.
  </Accordion>

  <Accordion title="Native Android">
    Revyl injects launch variables into Android as string extras on the app launch intent.

    ```kotlin theme={null}
    private val allowedRedirects = mapOf(
      "/account" to AppRoute.Account,
      "/checkout" to AppRoute.Checkout,
      "/cart" to AppRoute.Cart,
    )
    private val allowedRoles = setOf("buyer", "support")

    data class RevylAuthConfig(val enabled: Boolean, val token: String?)

    fun revylAuthConfig(intent: Intent): RevylAuthConfig {
      return RevylAuthConfig(
        enabled = intent.getStringExtra("REVYL_AUTH_BYPASS_ENABLED") == "true",
        token = intent.getStringExtra("REVYL_AUTH_BYPASS_TOKEN"),
      )
    }

    fun handleRevylAuthBypass(intent: Intent, config: RevylAuthConfig) {
      val uri = intent.data ?: return
      if (uri.scheme != "myapp" || uri.host != "revyl-auth") return

      val role = uri.getQueryParameter("role") ?: "buyer"
      val redirect = uri.getQueryParameter("redirect") ?: "/account"

      check(config.enabled) { "Revyl auth bypass is disabled" }
      check(uri.getQueryParameter("token") == config.token) { "Bad Revyl auth bypass token" }
      check(role in allowedRoles) { "Role is not allowlisted" }

      val route = allowedRedirects[redirect] ?: error("Redirect is not allowlisted")
      TestSession.signIn(role)
      appRouter.navigate(route)
    }
    ```

    Store `revylAuthConfig(launchIntent)` when the app starts, then call `handleRevylAuthBypass(intent, config)` from `onCreate` and `onNewIntent`.
  </Accordion>

  <Accordion title="Flutter">
    Flutter apps usually handle the link in Dart and read launch config through a platform channel or staging backend.

    ```dart theme={null}
    import 'package:app_links/app_links.dart';
    import 'package:flutter/services.dart';

    const launchConfig = MethodChannel('app.launchConfig');
    final allowedRedirects = {
      '/account': AppRoute.account,
      '/checkout': AppRoute.checkout,
      '/cart': AppRoute.cart,
    };
    final allowedRoles = {'buyer', 'support'};

    Future<void> handleRevylAuthBypass(Uri uri) async {
      if (uri.scheme != 'myapp' || uri.host != 'revyl-auth') return;

      final config = await launchConfig.invokeMapMethod<String, String>('revylAuthBypass');
      final enabled = config?['REVYL_AUTH_BYPASS_ENABLED'] == 'true';
      final expectedToken = config?['REVYL_AUTH_BYPASS_TOKEN'];
      final role = uri.queryParameters['role'] ?? 'buyer';
      final redirect = uri.queryParameters['redirect'] ?? '/account';

      if (!enabled) throw StateError('Revyl auth bypass is disabled');
      if (uri.queryParameters['token'] != expectedToken) throw StateError('Bad Revyl auth bypass token');
      if (!allowedRoles.contains(role)) throw StateError('Role is not allowlisted');

      final route = allowedRedirects[redirect];
      if (route == null) throw StateError('Redirect is not allowlisted');

      await TestSession.signIn(role: role);
      appRouter.go(route);
    }

    Future<void> installRevylAuthBypass() async {
      final links = AppLinks();
      final initial = await links.getInitialLink();
      if (initial != null) await handleRevylAuthBypass(initial);
      links.uriLinkStream.listen(handleRevylAuthBypass);
    }
    ```

    Implement the platform channel with the native iOS and Android launch-config snippets above, or verify the token against your staging backend from Dart.
  </Accordion>
</AccordionGroup>

## Mint The Token Per Session

The contract above expects `REVYL_AUTH_BYPASS_TOKEN` to already exist as an
organization launch variable. That works while the token is long-lived, but
breaks as soon as it expires or has to be minted per run. Add
`session.before_script` and the CLI mints it for you before every device
session. For the exact attach order on `revyl device start` (config discovery,
mint → launch env → deep link), see
[Auth And Session Prep](/cli/device/auth-and-session-prep).

```yaml theme={null}
session:
  before_script:
    script_path: "./scripts/prepare-test-session.sh"
    timeout_seconds: 120 # optional; defaults to 120
  auth_bypass:
    launch_vars: [REVYL_AUTH_BYPASS_ENABLED, REVYL_AUTH_BYPASS_TOKEN]
    deep_link: "myapp://revyl-auth?token=${REVYL_AUTH_BYPASS_TOKEN}"
```

`session.before_script` produces values and `session.auth_bypass` consumes them.

The script is any executable resolved relative to the Revyl project root (the
directory that owns `.revyl/`) and run with that directory as the working
directory. The path may leave the project directory via `../` as long as it
stays inside the git worktree. Every `KEY=VALUE` line it prints on standard
output becomes a launch variable for that one session; every other line is
ignored, so it can log freely:

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

echo "Minting a session token..."   # ignored: not KEY=VALUE
TOKEN="$(curl -fsSL --max-time 20 "$STAGING_URL/test-tokens" | jq -r .token)"

echo "REVYL_AUTH_BYPASS_ENABLED=true"
echo "REVYL_AUTH_BYPASS_TOKEN=$TOKEN"
```

Because the values belong to the session rather than your organization,
concurrent runs never overwrite each other's tokens. That is what makes it safe
for several pull requests to prove themselves at the same time.

<Note>
  * A non-zero exit fails session start. Validating an unprepared app is worse
    than not validating it, so setup failures are never downgraded to warnings. -
    A deep link must take all of its `${VAR}` placeholders from the script or none
    of them. Mixing the two sources is rejected before the session starts, because
    only organization launch variables resolve server-side. - Keep the script
    executable (`chmod +x`). Resolve it relative to the Revyl project root; it may
    leave that directory via `../` but paths outside the git worktree are
    rejected. - Depend only on credentials already in the environment, such as
    `REVYL_API_KEY`. A coding agent running `revyl device start` in a fresh
    checkout has no other repository secrets.
</Note>

The script runs wherever the CLI runs: your machine, your CI, or a coding
agent's checkout. It does not run for the preview link in a pull request
comment, which has no checkout; that path still reads organization launch
variables. Values never appear in CLI output, and `revyl dev status` reports
the produced key names only (the runtime status field remains `before_session`):

```json theme={null}
{
  "before_session": {
    "configured": true,
    "script": "./scripts/prepare-test-session.sh",
    "keys": ["REVYL_AUTH_BYPASS_ENABLED", "REVYL_AUTH_BYPASS_TOKEN"],
    "state": "ready"
  }
}
```

`revyl dev auth refresh` re-fires the configured deep link; it does not remint
before-script values. Launch environment is fixed at session start. If a
minted token expired, run `revyl dev stop` then `revyl dev` so a fresh mint is
applied.

## What To Verify

Before relying on the bypass in a test, confirm each case is visible:

* Valid token, role, and redirect signs in and routes to the target screen.
* Wrong token is rejected.
* Missing or disabled `REVYL_AUTH_BYPASS_ENABLED` is rejected.
* Unknown role is rejected.
* Unknown redirect is rejected.
* The handler is unavailable in production builds.

## Bug Bazaar Sample

This repo includes the runnable Expo sample in `internal-apps/bug-bazaar`:

```bash theme={null}
revyl device navigate \
  --url "bug-bazaar://revyl-auth?token=revyl-demo-token&role=collector&redirect=%2Fcheckout"
```

The Account tab shows whether the auth bypass link is idle, accepted, or rejected.

Bug Bazaar is a managed Expo sample, so it uses a demo fallback token when no native launch-config bridge is present. Customer apps should wire `REVYL_AUTH_BYPASS_TOKEN` into native launch config or verify the token with a staging backend.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.