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

Prompt for your coding agent:

```text
Add a test-only auth bypass deep link to this app, so Revyl sessions can start signed in.

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

Install the `revyl-cli-auth-bypass` skill for this agent and follow it. Gate the link with the `REVYL_AUTH_BYPASS_ENABLED` and `REVYL_AUTH_BYPASS_TOKEN` launch variables, keep it out of production builds, and never commit a real token.

You're done when every check in the page's What To Verify section passes.

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

## Fastest Path For Expo Cloud Agents

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

```bash
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
myapp://revyl-auth?token=<token>&role=<role>&redirect=<allowlisted-route>
```

Create these launch variables:

```bash
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
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
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
    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
    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
    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
    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
    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 iOS or
Android device session. Web (alpha) starts do not run `session.before_script`; use
[`session.auth_bypass.sign_in`](/devices/session-prep#web-sign-in-links)
for worker-minted Web sign-in, and make sure any data a Web proof needs already
exists in the preview environment, for example seeded by the preview's own
deploy or a test-login route. For the exact attach order on
`revyl device start` (config discovery, mint → launch env → deep link), see
[Auth And Session Prep](/devices/session-prep).

```yaml
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
#!/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
{
  "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.

## Give Proofs An Account That Can Reach The Change

Auth bypass decides which account a session signs in to. A proof of changes
can only exercise the screens that account can reach. A bypass can work and
still block the proof when the account is in the wrong state. For example, it
may still be in a setup or calibration period, have no data for the changed
screens, or have already finished the onboarding a pull request changes.

Use one of these, simplest first:

- **A dedicated proof account.** Create a non-production account that already
  has the data, progress, and permissions your proofs need. Use it only for
  Revyl sessions, so people and other tests do not change its state.
- **Reset the account when the link signs in.** Have the `revyl-auth` handler
  call a staging-only endpoint that checks the bypass token, resets or seeds
  the proof account, and returns its session. For example, the endpoint can
  finish calibration, add sample data, or clear onboarding.
- **Reset it before the device starts.** Add a `session.before_script` that
  resets or seeds the account and prints the launch values for that session
  (see [Mint The Token Per Session](#mint-the-token-per-session)). The script
  runs in the proof's checkout with only credentials already in its
  environment, such as `REVYL_API_KEY`, so the endpoint it calls cannot need
  another repository secret.
- **One account per state.** Use the contract's `role` parameter to choose
  between prepared accounts, for example `role=onboarded` and `role=new-user`.
  Allowlist each role in the handler.

When an iOS or Android proof stops at sign-in, or its account cannot reach the
change, the pull request comment adds a collapsed setup section under the proof
that links back here.

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

The public [Bug Bazaar](https://github.com/RevylAI/bug-bazaar) repository is a runnable Expo sample of
this pattern:

```bash
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.
