@repo/auth-mobile
Mobile authentication with biometric support, OAuth deep linking, and secure storage for React Native. Face ID login, Google/GitHub SSO, encrypted session storage. Works with Better Auth and Expo.
Quick Start
Add mobile authentication in 10 minutes:
pnpm add @repo/auth-mobileBiometric auth, OAuth deep linking, secure storage. Skip to Quick Start →
Why @repo/auth-mobile?
Web authentication doesn't translate to mobile apps. OAuth redirects break in native apps without deep linking setup. AsyncStorage isn't secure for storing tokens. Face ID and Touch ID require platform-specific code. Session management differs on mobile (no cookies). Better Auth needs mobile-specific adapters.
@repo/auth-mobile solves this with Better Auth mobile adapter, Expo deep linking integration, secure enclave storage, and biometric authentication.
Production-ready with Expo SecureStore encryption, Face ID/Touch ID support, OAuth callback handling, and type-safe session management.
Use cases
- Mobile apps with social login — OAuth with GitHub, Google, Apple using deep links
- Biometric authentication — Face ID, Touch ID, fingerprint with fallback to passcode
- Secure token storage — Encrypted storage using device secure enclave
- Protected routes — Route guards with Expo Router and useSession hook
- Cross-platform auth — Share auth logic between iOS and Android
How it works
@repo/auth-mobile extends Better Auth with mobile-specific features:
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store";
export const authClient = createAuthClient({
baseURL: process.env.EXPO_PUBLIC_API_BASE_URL,
plugins: [
expoClient({
scheme: "myapp", // Your app's URL scheme for OAuth
storagePrefix: "myapp-auth",
storage: SecureStore // Encrypted storage
})
]
});
// OAuth automatically handles deep links: myapp://auth/callback
// Sessions stored encrypted in secure enclave
// Biometric auth protects sensitive operationsUses Better Auth React client with Expo plugin, Expo SecureStore for encrypted token storage, Expo WebBrowser for OAuth flows, and Expo LocalAuthentication for biometrics.
Key features
OAuth deep linking — GitHub, Google, Apple sign-in with automatic callback handling
Biometric authentication — Face ID, Touch ID, fingerprint with device passcode fallback
Secure storage — Encrypted token storage using iOS Keychain and Android Keystore
Session management — useSession hook with automatic token refresh
Protected routes — Expo Router integration with authentication guards
Type-safe — Full TypeScript support with Better Auth types
Quick Start
1. Install the package and Expo dependencies
pnpm add @repo/auth-mobile expo-secure-store expo-local-authentication expo-web-browser2. Configure auth client with URL scheme
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store";
export const authClient = createAuthClient({
baseURL: process.env.EXPO_PUBLIC_API_BASE_URL,
plugins: [
expoClient({
scheme: "myapp", // Must match app.config.ts
storagePrefix: "myapp-auth",
storage: SecureStore
})
]
});
export const { signIn, signUp, signOut, useSession } = authClient;export default {
scheme: "myapp", // OAuth callbacks use: myapp://auth/callback
ios: {
associatedDomains: ["applinks:yourdomain.com"]
}
};3. Add OAuth provider with deep linking
import { signIn } from "#/lib/auth-client";
import * as WebBrowser from "expo-web-browser";
import * as Linking from "expo-linking";
import { Button } from "@repo/uni-ui";
// Handle OAuth callback
WebBrowser.maybeCompleteAuthSession();
export function LoginScreen() {
const handleGitHubLogin = async () => {
await signIn.social({
provider: "github",
callbackURL: Linking.createURL("/auth/callback"),
});
};
return <Button onPress={handleGitHubLogin}>Sign in with GitHub</Button>;
}import { useEffect } from "react";
import { router } from "expo-router";
export default function AuthCallback() {
useEffect(() => {
// Better Auth handles the callback automatically
router.replace("/");
}, []);
return <LoadingScreen />;
}4. Protect routes with useSession
import { useSession } from "#/lib/auth-client";
import { Redirect, Stack } from "expo-router";
export default function AuthLayout() {
const { data: session, isPending } = useSession();
if (isPending) return <SplashScreen />;
if (!session) {
return <Redirect href="/login" />;
}
return <Stack />;
}That's it! Your mobile app now has OAuth authentication with deep linking and secure session storage.
Add biometric authentication
Protect sensitive operations with Face ID/Touch ID:
import { authenticateWithBiometric } from "@repo/auth-mobile/biometric";
const result = await authenticateWithBiometric({
promptMessage: "Authenticate to view balance"
});
if (result.success) {
// Show sensitive data
}Technical Details
For Developers: Technical implementation details
Overview
| Property | Value |
|---|---|
| Location | packages/auth-mobile |
| Dependencies | better-auth, expo-secure-store, expo-local-authentication |
| Platform | React Native, Expo |
Export Paths
| Path | Description |
|---|---|
@repo/auth-mobile | Main exports |
@repo/auth-mobile/client | Auth client for mobile |
@repo/auth-mobile/biometric | Biometric authentication |
Authentication Flows
Email/Password
import { signIn, signUp } from "#/lib/auth-client";
// Sign up
const result = await signUp.email({
email: "user@example.com",
password: "securePassword123",
name: "John Doe"
});
// Sign in
const result = await signIn.email({
email: "user@example.com",
password: "securePassword123"
});OAuth with Deep Links
Deep Link Setup
Configure your app's URL scheme in app.config.ts before using OAuth. See the Deep Linking section below.
import { signIn } from "#/lib/auth-client";
import * as WebBrowser from "expo-web-browser";
import * as Linking from "expo-linking";
// highlight-next-line
// Handle OAuth callback
WebBrowser.maybeCompleteAuthSession();
export async function signInWithGitHub() {
await signIn.social({
provider: "github",
// highlight-next-line
callbackURL: Linking.createURL("/auth/callback")
});
}Biometric Authentication
Platform Support
Biometric authentication uses Face ID on iOS, fingerprint on Android, and falls back to device passcode when biometrics aren't available.
Check Availability
import { isBiometricAvailable, getBiometricType } from "@repo/auth-mobile/biometric";
const available = await isBiometricAvailable();
// highlight-next-line
const type = await getBiometricType(); // "fingerprint" | "facial" | "iris" | nullAuthenticate
import { authenticateWithBiometric } from "@repo/auth-mobile/biometric";
const result = await authenticateWithBiometric({
// highlight-start
promptMessage: "Authenticate to continue",
cancelLabel: "Cancel",
disableDeviceFallback: false
// highlight-end
});
if (result.success) {
// User authenticated
await loadSecureData();
} else {
console.log("Authentication failed:", result.error);
}Biometric-Protected Session
import { useBiometricSession } from "@repo/auth-mobile/biometric";
export function ProtectedScreen() {
const { isAuthenticated, authenticate, isLoading } = useBiometricSession({
// highlight-next-line
timeout: 5 * 60 * 1000, // Re-auth after 5 minutes
});
if (!isAuthenticated) {
return (
<Button onPress={authenticate} title="Unlock with Face ID" />
);
}
return <SecureContent />;
}Secure Storage
Encryption
All data stored with secureStore is encrypted using the device's secure enclave. Never store sensitive data in
AsyncStorage.
Store Credentials
import { secureStore } from "@repo/auth-mobile";
// Store
await secureStore.set("api_key", "sk-xxx");
// Retrieve
const apiKey = await secureStore.get("api_key");
// Delete
await secureStore.delete("api_key");Encrypted Session
import { encryptedSession } from "@repo/auth-mobile";
// Session is automatically encrypted and stored
const session = await encryptedSession.get();
if (session) {
console.log("User:", session.user.email);
}Session Management
useSession Hook
import { useSession } from "#/lib/auth-client";
export function ProfileScreen() {
// highlight-next-line
const { data: session, isPending, error, refetch } = useSession();
if (isPending) {
return <ActivityIndicator />;
}
if (!session) {
return <LoginPrompt />;
}
return (
<View>
<Text>Welcome, {session.user.name}</Text>
<Button onPress={refetch} title="Refresh" />
</View>
);
}Protected Routes (Expo Router)
// app/(auth)/_layout.tsx
import { useSession } from "#/lib/auth-client";
import { Redirect, Stack } from "expo-router";
export default function AuthLayout() {
const { data: session, isPending } = useSession();
if (isPending) {
return <SplashScreen />;
}
// highlight-start
if (!session) {
return <Redirect href="/login" />;
}
// highlight-end
return <Stack />;
}Deep Linking
App Configuration
// app.config.ts
export default {
// highlight-next-line
scheme: "myapp",
ios: {
associatedDomains: ["applinks:yourdomain.com"]
},
android: {
intentFilters: [
{
action: "VIEW",
data: [{ scheme: "myapp" }],
category: ["BROWSABLE", "DEFAULT"]
}
]
}
};Handle Auth Callback
// app/auth/callback.tsx
import { useEffect } from "react";
import { useLocalSearchParams, router } from "expo-router";
import { authClient } from "#/lib/auth-client";
export default function AuthCallback() {
const params = useLocalSearchParams();
useEffect(() => {
// Better Auth handles the callback automatically
// Just redirect to home
router.replace("/");
}, []);
return <LoadingScreen />;
}Environment Variables
# API URL
EXPO_PUBLIC_API_BASE_URL="https://api.yourdomain.com"
# OAuth (configured on server)
# Client only needs the base URLRelated Packages
- @repo/auth - Web authentication
- @repo/security - Security utilities