---
title: React Native / Expo
description: convex-auth in a React Native or Expo app.
sidebar:
  icon: smartphone
  order: 2
---

`@vortex-api/convex-auth/react-native` is the Expo / React Native client. It mirrors `@vortex-api/convex-auth/react` but uses React Native-compatible storage and screen components.

## Install

```bash
pnpm add @vortex-api/convex-auth convex
# or, if you use Expo:
pnpm add expo-secure-store expo-web-browser
```

The React Native components and example use **Uniwind** (Tailwind CSS v4 for React Native) and the convex-auth UI tokens:

```bash
pnpm add tailwindcss uniwind
```

## Metro and CSS entry

Add Uniwind to `metro.config.js`:

```js metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withUniwindConfig } = require("uniwind/metro");

module.exports = withUniwindConfig(getDefaultConfig(__dirname), {
  cssEntryFile: "./src/global.css",
  debug: false,
  inlineRem: 16,
});
```

Create a CSS entry that imports Tailwind, Uniwind, and the auth package UI styles:

```css src/global.css
@import "tailwindcss";
@import "uniwind";
@import "@vortex-api/convex-auth/ui/styles.css";

/* Allow Uniwind to find the class names used by the auth package. */
@source "../../../packages/auth/src";
```

Import the CSS entry at the top of your root component:

```tsx src/App.tsx
import "./global.css";
```

The auth package already includes a `uniwind-env.d.ts` declaration that adds `className` props to React Native components, so the above classes are typed.

## Provider

Use `ExpoConvexAuthClientProvider` with a SecureStore-backed storage object. The provider also needs a way to receive deep links (OAuth, password reset, email verification, etc.):

```tsx src/Root.tsx
import * as Linking from "expo-linking";
import * as SecureStore from "expo-secure-store";
import { ConvexReactClient, ConvexProvider } from "convex/react";
import {
  ExpoConvexAuthClientProvider,
  type NativeAuthActions,
} from "@vortex-api/convex-auth/react-native";
import { api } from "../convex/_generated/api";

const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!);

const storage = {
  getItem: (key: string) => SecureStore.getItemAsync(key),
  setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
  deleteItem: (key: string) => SecureStore.deleteItemAsync(key),
  setItemAsync: async (key: string, value: string) => {
    await SecureStore.setItemAsync(key, value);
  },
  deleteItemAsync: async (key: string) => {
    await SecureStore.deleteItemAsync(key);
  },
};

function Root() {
  return (
    <ConvexProvider client={convex}>
      <ExpoConvexAuthClientProvider
        actions={api.auth as unknown as NativeAuthActions}
        storage={storage}
        initialUrl={Linking.createURL("/")}
        subscribeToUrl={(handler) => {
          const subscription = Linking.addEventListener("url", ({ url }) => handler(url));
          return () => subscription.remove();
        }}
      >
        <App />
      </ExpoConvexAuthClientProvider>
    </ConvexProvider>
  );
}
```

`ExpoConvexAuthClientProvider` accepts a `storage` object with `getItem`, `setItem`, `deleteItem`, `setItemAsync`, and `deleteItemAsync` to persist the token, refresh token, and session id. `expo-secure-store` is the recommended implementation.

## Sign in, sign up, and 2FA

Use the drop-in screen components:

```tsx src/SignIn.tsx
import { ExpoAuthClientSignInScreen } from "@vortex-api/convex-auth/react-native";

export function SignIn() {
  return (
    <ExpoAuthClientSignInScreen
      signUpUrl=""
      forceRedirectUrl={Linking.createURL("/")}
      title="Sign in"
      description="Sign in with Google, GitHub, Discord, or email."
    />
  );
}
```

`ExpoAuthClientSignInScreen` and `ExpoAuthClientSignUpScreen` already handle the 2FA step-up internally. If `signIn.email` returns `twoFactorRedirect`, they render `ConvexVerifyTwoFactorForm` automatically.

## OAuth in React Native

For Google, GitHub, or Discord in React Native, `ExpoAuthClientSignInScreen` calls `signIn.social` and opens the returned OAuth URL with `expo-web-browser` `openAuthSessionAsync`. The OAuth provider is configured to redirect to `https://<your-deployment>.convex.site/api/auth/callback/<provider>`; the Convex auth server then redirects back to your app's URL scheme with the session token, refresh token, and session id as query parameters.

Register your scheme in `app.json`:

```json app.json
{
  "expo": {
    "scheme": "convex-auth-rn",
    "ios": { "bundleIdentifier": "nyc.vortex.convexauth.demo" },
    "android": { "package": "nyc.vortex.convexauth.demo" }
  }
}
```

`ExpoConvexAuthClientProvider` receives the callback through `subscribeToUrl` and completes the session.

Expo Go uses the `exp://` scheme, so in development `Linking.createURL("/")` returns an `exp://.../--/` URL. Expo Go can be used for quick tests, but for a real OAuth app registration you should build a development or standalone client so the custom scheme belongs to your app.

## Other auth flows

The package ships additional form components for the flows that are usually reached by deep link:

| Component                   | Use                                                                                                                     |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `ConvexForgotPasswordForm`  | Request a password reset email. Pass `resetPasswordUrl` (the deep link the reset email should point at).                |
| `ConvexResetPasswordForm`   | Set a new password after the user opens the reset deep link. Pass `token` from the deep link.                           |
| `ConvexVerifyEmailScreen`   | Verify an email from a deep link. Pass `token`; optionally pass `userEmail` and `resendCallbackUrl` to allow resending. |
| `ConvexVerifyTwoFactorForm` | Collect TOTP or backup code during sign-in. Usually handled by `ExpoAuthClientSignInScreen`.                            |
| `ConvexEnableTwoFactorForm` | Enroll TOTP on a signed-in account. Pass `issuer` and, optionally, `renderQR` for a QR component.                       |

Example deep link parsing for reset and verify:

```tsx src/useAuthDeepLink.ts
import * as Linking from "expo-linking";

export type AuthDeepLink =
  { screen: "reset"; token: string } | { screen: "verify"; token: string } | null;

export function parseAuthDeepLink(url: string | null): AuthDeepLink {
  if (typeof url !== "string" || url.length === 0) return null;
  const parsed = Linking.parse(url);
  const path = parsed.path?.toLowerCase() ?? "";
  const token = typeof parsed.queryParams?.token === "string" ? parsed.queryParams.token : "";

  if (path === "reset-password" && token.length > 0) {
    return { screen: "reset", token };
  }
  if (path === "verify-email" && token.length > 0) {
    return { screen: "verify", token };
  }
  return null;
}
```

Render the matching component when a deep link is received:

```tsx src/Reset.tsx
import { ConvexResetPasswordForm } from "@vortex-api/convex-auth/react-native";

export function Reset({ token }: { token: string }) {
  return (
    <ConvexResetPasswordForm
      token={token}
      onReset={() => {
        // navigate to sign-in
      }}
    />
  );
}
```

## Passkeys

Native passkey ceremonies use the optional `react-native-passkeys` peer dependency and the dedicated subpath export:

```sh
pnpm add react-native-passkeys
```

```tsx
import { usePasskeys } from "@vortex-api/convex-auth/react-native/passkeys";

const passkeys = usePasskeys({ userId: user.id, identifier: user.email ?? "" });

await passkeys.register("This device"); // requires a signed-in user
await passkeys.signIn();
```

Both platforms need a domain-association file served from your `rpID` — the Apple App Site Association file on iOS and `assetlinks.json` on Android — plus the platform origin entries in `passkey.origin`. If a passkey sign-in resolves to `twoFactorRedirect` (relaxed `userVerification` on a 2FA-enabled account), render `ConvexVerifyTwoFactorForm` to complete the challenge. The full setup — entitlements, AASA/assetlinks hosting, signing-fingerprint origins, and emulator caveats — is in the Passkeys guide.

`react-native-passkeys` decorates credential responses with methods such as `getPublicKey()`; `usePasskeys` strips function-valued properties before sending them over the wire. This is required, not cosmetic: upstream `convex-js` currently crashes while formatting the "unsupported type" error for function-valued arguments (`JSON.stringify` returns `undefined` and `.length` throws), masking the real message — [get-convex/convex-js#190](https://github.com/get-convex/convex-js/issues/190). If you build a custom passkey call path, strip functions the same way rather than passing credential objects through verbatim.

## Styling

The screen components support a `styles` prop for every keyed element (`root`, `title`, `input`, `submitButton`, etc.). The components also use Uniwind `className` strings internally, so styling works out of the box if you imported the convex-auth CSS.

```tsx
<ExpoAuthClientSignInScreen
  styles={{
    root: { padding: 24, borderRadius: 12 },
    submitButton: { backgroundColor: "#0f172a" },
  }}
/>
```

Use the design tokens from `@vortex-api/convex-auth/ui/styles.css` in your own components instead of hard-coding colors. The components reference tokens like `bg-background`, `text-foreground`, `border-input`, and `text-destructive`.

## Available components

- `ExpoConvexAuthClientProvider`
- `ExpoAuthClientSignInScreen`
- `ExpoAuthClientSignUpScreen`
- `ConvexForgotPasswordForm`
- `ConvexResetPasswordForm`
- `ConvexVerifyEmailScreen`
- `ConvexVerifyTwoFactorForm`
- `ConvexEnableTwoFactorForm`
- `ConvexSessionList`
- `ConvexChangeEmailForm`
- `ConvexProfileEditForm`
- `ConvexProfileImageUploader`
- `ConvexCreateOrganization`
- `ConvexOrganizationList`
- `ConvexOrganizationMembers`
- `ConvexOrganizationProfile`
- `ConvexOrganizationSwitcher`

See the [React Native component reference](/convex-auth/components/react-native) for the component list.

## Example apps

- **`examples/react-native`** — minimal Expo app wired with `ExpoConvexAuthClientProvider`, SecureStore storage, and the drop-in screens.

  ```bash
  cd examples/react-native
  pnpm install
  pnpm start   # expo start
  ```

- **`examples/react-native-web`** — the same RN components running in the browser via `react-native-web`.

  ```bash
  cd examples/react-native-web
  pnpm install
  pnpm run dev
  ```
