ZevAuth
Sign up

Building with the SDK

Expo and React Native

Native sign-in for iOS and Android with Expo: the provider, hooks, screens, deep links, Sign in with ZevID, theming and wording.

npx expo install @zevauth/expo expo-secure-store expo-web-browser expo-crypto

Works with Expo SDK 56, 57 and 58. We support the current SDK and the two before it, and add each new SDK as Expo releases it. npx expo install picks the module versions that match your SDK.

@zevauth/expo gives a native app what @zevauth/react gives a website: the same hooks, ready-made screens, and a session that survives restarts. It’s built on the web package’s DOM-free half, so useUser() behaves identically in your web app and your mobile app.

The provider

Wrap your app once, usually in the root layout:

// app/_layout.tsx (Expo Router)
import { Slot } from 'expo-router';
import { ZevAuthProvider } from '@zevauth/expo';

export default function RootLayout() {
  return (
    <ZevAuthProvider
      publishableKey={process.env.EXPO_PUBLIC_ZEVAUTH_KEY!}
      redirectUrl="https://acme.example/auth/callback"
    >
      <Slot />
    </ZevAuthProvider>
  );
}
PropMeaning
publishableKeypk_test_… or pk_live_…. Required unless you pass client.
redirectUrlWhere sign-in links and ZevID return to your app. See Deep links. Required.
apiUrlWhere the API lives. Defaults to https://api.zevauth.net.
appearanceHow the screens look. See Customising.
localizationThe words on the screens. See Wording.
storage, crypto, browserReplacements for the Expo modules, for bare React Native or tests.
clientAn existing client from createZevAuth, instead of a key.

The provider:

  • Stores the session securely. The refresh token goes in the Keychain (iOS) or Keystore (Android), on this device only, so it isn’t restored from a backup onto another phone. The access token stays in memory.
  • Finishes links automatically. When a link opens the app, whether a sign-in link, an email confirmation or a ZevID return, the sign-in completes without any code of yours.
  • Re-checks on return. Each time the app comes back to the foreground, the session is checked. A session ended elsewhere signs out here too, for example after “sign out everywhere” or a password change.

Hooks and control components

These are @zevauth/react’s own, re-exported:

import { useUser, useAuth, SignedIn, SignedOut, Protect } from '@zevauth/expo';

const { isLoaded, isSignedIn, user } = useUser();
const { userId, organizationId, getToken, signOut } = useAuth();

getToken() returns the access token for your own API, refreshing it first if it’s about to expire. Verify it on your server with @zevauth/backend, exactly as for a web app.

Screens

import { SignIn, SignUp, UserButton, UserProfile } from '@zevauth/expo';

<SignIn onSignedIn={() => router.replace('/')} />
<SignUp onSignedUp={() => router.replace('/')} />
<UserButton onManageAccount={() => router.push('/account')} />
<UserProfile showSessions />
ScreenWhat it does
<SignIn>Whichever of password, email code, sign-in link and ZevID your environment enables. Then the second step for anyone with two-step verification, and setup when your app requires it.
<SignUp>Creates the account and confirms the email with the emailed code.
<UserButton>An avatar that opens a sheet with the person’s name and Sign out. onManageAccount adds “Manage account”.
<UserAvatar>The picture, or initials.
<UserProfile>Name, password, two-step verification, and with showSessions the person’s signed-in devices.
<TaskSetupMfa>Two-step setup on its own, for an app that routes signed-in people elsewhere.

The screens never navigate. Each takes a callback such as onSignedIn, so your router stays in charge.

Two-step setup on a phone has no QR code, because the person is holding the device it would be scanned with. “Open authenticator app” hands the setup link to whichever authenticator is installed, and the key is shown for typing in by hand.

Sign-in links, email confirmations and Sign in with ZevID all come back to your app through redirectUrl.

In development, use your app’s custom scheme, such as acme://auth/callback. Set scheme in app.json, then add the callback under Project settings → Domains & callbacks in the console. Custom schemes work in development environments only, because any app on a phone can claim a scheme.

In production, use a universal link (iOS) or app link (Android) on your verified domain, such as https://acme.example/auth/callback:

  1. Register the apps. In the console under Project settings → Mobile apps, add each app: on iOS the bundle ID and Apple team ID, on Android the package name and SHA-256 signing certificate fingerprint.
  2. Serve the association files. Host the two files the console generates for your domain: /.well-known/apple-app-site-association and /.well-known/assetlinks.json. Then press Check again until both apps show as verified.
  3. Declare the domain in your app.
{
  "expo": {
    "scheme": "acme",
    "ios": { "associatedDomains": ["applinks:acme.example"] },
    "android": {
      "intentFilters": [{
        "action": "VIEW",
        "autoVerify": true,
        "data": [{ "scheme": "https", "host": "acme.example", "pathPrefix": "/auth/callback" }],
        "category": ["BROWSABLE", "DEFAULT"]
      }]
    }
  }
}
  1. Register the callback. Add https://acme.example/auth/callback under Domains & callbacks.

See Mobile apps for how verification works and what people see.

Sign in with ZevID

The ZevID button opens the system authentication session: ASWebAuthenticationSession on iOS, Custom Tabs on Android. It never uses an embedded web view. The system browser shares the person’s existing ZevID session and password manager, and your app can’t read what they type.

  • No second sign-in. Someone already signed in to ZevID in Safari or Chrome continues without signing in again.
  • Tied to your app. Each sign-in uses a PKCE challenge and a state value. The code that comes back can only be exchanged by your app, for the exact redirectUrl it was sent to.
  • Closing is safe. If the person closes the sheet, the form stays as it was.
  • Your name on the consent screen. With your apps registered and verified, ZevID’s consent screen shows your app’s name with the device, for example “Acme on iPhone”.

Customising

appearance uses the same ideas as the web, in native units:

<ZevAuthProvider
  publishableKey="pk_live_..."
  redirectUrl="https://acme.example/auth/callback"
  appearance={{
    theme: 'soft',
    colorScheme: 'system',
    variables: {
      colorPrimary: { light: '#4f46e5', dark: '#a5b4fc' },
      fontFamily: 'Inter',
      fontSize: 16,
      borderRadius: 10,
    },
    elements: {
      buttonPrimary: { paddingVertical: 14 },
      title: { letterSpacing: -0.3 },
    },
  }}
>
  • theme takes the same five names as the web: default, soft, minimal, sharp and contrast.
  • variables take the web’s colour names, each a value or a { light, dark } pair. fontSize and borderRadius are numbers, in points.
  • elements are React Native style objects, keyed by the web’s element names (card, title, input, buttonPrimary, zevIdButton and so on). One design spec covers both apps. Native adds buttonText and codeInput.
  • The console’s settings still apply. Your accent, logo, radius and wording come from the console unless you override them here.

Wording

localization takes exactly the web’s shape, so a translation written for your web app works unchanged. A native group covers the few phone-only phrases:

localization={{
  common: { email: 'E-mail', password: 'Mot de passe' },
  signIn: { submit: 'Se connecter' },
  native: { openAuthenticator: "Ouvrir l'application d'authentification" },
}}

Bare React Native

The package uses three Expo modules: expo-secure-store, expo-web-browser and expo-crypto. In a bare React Native app, add them with npx install-expo-modules, or pass your own storage, crypto and browser adapters to the provider.

A working example

zevauth-examples/expo is a minimal Expo Router app with sign-in, the account button and the profile with devices.

Updated at, Sunday, November 1, 2026