OneApp Docs
PackagesAuth

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

Biometric 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 operations

Uses 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-browser

2. Configure auth client with URL scheme

lib/auth-client.ts
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;
app.config.ts
export default {
  scheme: "myapp", // OAuth callbacks use: myapp://auth/callback
  ios: {
    associatedDomains: ["applinks:yourdomain.com"]
  }
};

3. Add OAuth provider with deep linking

components/LoginScreen.tsx
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>;
}
app/auth/callback.tsx
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

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 />;

  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

PropertyValue
Locationpackages/auth-mobile
Dependenciesbetter-auth, expo-secure-store, expo-local-authentication
PlatformReact Native, Expo

Export Paths

PathDescription
@repo/auth-mobileMain exports
@repo/auth-mobile/clientAuth client for mobile
@repo/auth-mobile/biometricBiometric 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"
});

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" | null

Authenticate

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 URL

On this page