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>
);
}
| Prop | Meaning |
|---|---|
publishableKey | pk_test_… or pk_live_…. Required unless you pass client. |
redirectUrl | Where sign-in links and ZevID return to your app. See Deep links. Required. |
apiUrl | Where the API lives. Defaults to https://api.zevauth.net. |
appearance | How the screens look. See Customising. |
localization | The words on the screens. See Wording. |
storage, crypto, browser | Replacements for the Expo modules, for bare React Native or tests. |
client | An 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 />
| Screen | What 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.
Deep links
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:
- 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.
- Serve the association files. Host the two files the console generates
for your domain:
/.well-known/apple-app-site-associationand/.well-known/assetlinks.json. Then press Check again until both apps show as verified. - 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"]
}]
}
}
}
- Register the callback. Add
https://acme.example/auth/callbackunder 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
statevalue. The code that comes back can only be exchanged by your app, for the exactredirectUrlit 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 },
},
}}
>
themetakes the same five names as the web:default,soft,minimal,sharpandcontrast.variablestake the web’s colour names, each a value or a{ light, dark }pair.fontSizeandborderRadiusare numbers, in points.elementsare React Native style objects, keyed by the web’s element names (card,title,input,buttonPrimary,zevIdButtonand so on). One design spec covers both apps. Native addsbuttonTextandcodeInput.- 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