Skip to content
v3.0.1 — MCP OAuth, organizations, passkeys, SSR adaptersChangelog
Convex Auth
Esc
↑↓navigate↵open⌘Jpreview
On this page

React Native / Expo

convex-auth in a React Native or Expo app.

@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

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:

pnpm add tailwindcss uniwind

Metro and CSS entry

Add Uniwind to 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:

@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:

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.):

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:

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:

{
  "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:

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:

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:

pnpm add react-native-passkeys
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. 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.

<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 for the component list.

Example apps

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

    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.

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

Was this page helpful?