Skip to main content

Overview

The Launch mobile app uses Better Auth with Expo integration for a secure, seamless authentication experience. This guide explains how authentication works in the mobile client and how it connects to the API.

Prerequisites

  • Backend running with auth routes enabled
  • EXPO_PUBLIC_API_URL set in apps/mobile/.env (from apps/mobile/example.env)

Steps

  1. Configure auth providers in the API: Backend Authentication Setup
  2. Verify mobile auth screens in apps/mobile/app/auth
  3. Test sign‑in on device or simulator

Architecture

Authentication Flow

Key Components

Auth Client

Location: apps/mobile/lib/auth/client.ts. Central authentication client that handles Better Auth requests, secure storage, and device headers.

Route Protection

Location: app/_layout.tsx and lib/hooks/useAppNavigation.ts. Navigation guards redirect users based on session and onboarding state.

Device Tracking

Location: apps/mobile/lib/utils/device-info.ts. Captures device information for security and analytics purposes.

Implementation Details

1. Auth Client Setup

The authentication client is configured in lib/auth/client.ts:
export const authClient = createAuthClient({
  // IMPORTANT: For mobile OAuth (Google), this must be reachable from your device.
  // Use your ngrok HTTPS URL during development (recommended).
  baseURL: `${process.env.EXPO_PUBLIC_API_URL}/api/auth`,
  plugins: [
    expoClient({
      scheme: "launch", // Deep link scheme
      storagePrefix: "launch", // SecureStore prefix
      storage: SecureStore, // Native secure storage
    }),
  ],
  fetchOptions: {
    customFetchImpl: async (url, init) => {
      const deviceHeaders = getDeviceHeaders();
      // Automatically inject device info in all requests
      return fetch(url, {
        ...init,
        headers: { ...init?.headers, ...deviceHeaders },
        credentials: "omit",
      });
    },
  },
});
Key Features:
  • Secure Storage: Uses Expo SecureStore for session persistence
  • Device Headers: Automatically includes device information in all requests
  • Deep Linking: Handles OAuth callbacks via launch:// scheme
  • Type Safety: Full TypeScript support with session hooks
Google OAuth (Android) requires a single origin.If your API is exposed via ngrok (e.g. https://xxxx.ngrok-free.app), then:
  • BETTER_AUTH_URL (API) must use that same origin
  • EXPO_PUBLIC_API_URL (mobile) must also use that same origin
If mobile starts auth at http://192.168.x.x:3001 but the callback returns to https://xxxx.ngrok-free.app, you will get a state mismatch error.

2. Route Protection

Authentication state determines which screens users can access. The current implementation handles redirects in useAppNavigation.ts and keeps the route stack defined in app/_layout.tsx. Benefits:
  • Centralized: Redirect rules live in one hook
  • Automatic: Users are routed based on auth and onboarding state
  • Type Safe: Expo Router provides full TypeScript support

3. Session Management

Sessions are managed by a lightweight provider that calls Better Auth to fetch the current session on app start and after auth events:
  • Persistence: Sessions survive app restarts via SecureStore
  • Manual refresh: SessionProvider calls authClient.getSession() on mount and exposes a refetch() helper used after sign-in/sign-out
  • Navigation: useAppNavigation derives the auth/onboarding target route

4. Device Information Tracking

Every authentication request includes device metadata:
export const getDeviceHeaders = (): Record<string, string> => {
  const deviceInfo = getDeviceInfo();

  return {
    "X-Device-Name": deviceInfo.deviceName || "unknown",
    "X-Device-Model": deviceInfo.deviceModel || "unknown",
    "X-OS-Version": deviceInfo.osVersion || "unknown",
    "X-App-Version": deviceInfo.appVersion || "unknown",
    "X-App-Platform": deviceInfo.platform,
    "X-Device-Type": deviceInfo.deviceType,
  };
};
Use Cases:
  • Security: Detect suspicious login patterns
  • Analytics: Understand user device distribution
  • Support: Debug issues specific to device types
  • Features: Enable/disable features based on device capabilities

Authentication Providers

Apple Sign-In

Configured for iOS devices with automatic availability detection:
// Check if Apple Sign-In is available
const isAvailable = await AppleAuthentication.isAvailableAsync();

// Handle Apple authentication
await authClient.signIn.social({
  provider: "apple",
  idToken: {
    token: credential.identityToken,
    nonce,
  },
  callbackURL: "/",
});
Features:
  • Native UI: Uses Apple’s native sign-in button
  • Secure: Implements proper nonce generation for security
  • Graceful Fallback: Shows standard button when unavailable

Google Sign-In

Implemented via Better Auth’s Expo client. The login screen calls authClient.signIn.social({ provider: "google" }), which opens the OAuth flow in a browser and returns to the app via the launch:// scheme.

Email Sign-In (OTP)

Email OTP sign-in is wired end-to-end. The flow uses Better Auth’s email OTP plugin and a dedicated verification screen:
  • apps/mobile/app/auth/email-signin.tsx (send code)
  • apps/mobile/app/auth/verify-email-otp.tsx (enter code)
  • docs/authentication/email-signin.mdx (setup details)

Platform-specific Behavior

iOS

  • Apple Sign-In uses the native expo-apple-authentication flow to obtain an Apple identity token and then calls Better Auth signIn.social({ provider: "apple", idToken }).
  • Google Sign-In uses Better Auth’s Expo client for browser-based OAuth and deep links back to the app via the launch:// scheme.

Android

  • Google Sign-In uses the same Better Auth Expo client flow as iOS (browser OAuth + deep link). Ensure your auth flow starts and ends on the same origin to avoid state mismatch errors.
  • Apple Sign-In is not available on Android.

Security Notes

Sessions are stored in SecureStore, OAuth redirects use the launch:// scheme, and device metadata is attached to auth requests for tracing and support.

Next Steps

  • Apple Sign-In Setup
  • Google Sign-In Setup
  • Email Sign-In with OTP
  • CORS Protection: Configured trusted origins prevent unauthorized access
  • Request Signing: Device headers provide request authenticity
  • Rate Limiting: Backend prevents brute force attacks
  • Error Logging: Comprehensive error tracking for security monitoring

Usage Examples

Checking Authentication State

import { authClient } from "@/lib/auth-client";

function MyComponent() {
  const { data: session, isPending } = authClient.useSession();

  if (isPending) return <LoadingSpinner />;

  if (session) {
    return <WelcomeMessage user={session.user} />;
  }

  return <LoginPrompt />;
}

Manual Sign Out

const handleSignOut = async () => {
  try {
    await authClient.signOut();
    // User automatically redirected to public routes
  } catch (error) {
    console.error("Sign out error:", error);
  }
};

Accessing User Data

const { data: session } = authClient.useSession();

if (session?.user) {
  const { name, email, image } = session.user;
  // Use user data in your components
}

Troubleshooting

Common Issues

Cause: SecureStore permissions or configuration issueSolution:
  1. Check that expo-secure-store is properly installed
  2. Verify the storage prefix matches your app configuration
  3. Test on a physical device (simulator may have limitations)
Cause: Missing Apple Developer configuration Solution: 1. Ensure usesAppleSignIn: true in app.config.ts 2. Add Apple Sign-In capability in Xcode 3. Configure Apple App ID with Sign-In capability 4. Set up proper environment variables (see Environment Setup)
Cause: Component not subscribed to auth state changesSolution:
  1. Use authClient.useSession() hook in your components
  2. Ensure components are wrapped in proper providers
  3. Check that React Query is configured correctly

Debug Mode

Enable detailed auth logging by setting:
// In development
if (__DEV__) {
  console.log("Auth Debug Mode Enabled");
  // Additional logging will appear in console
}

Environment Variables

The following environment variables are required in your API backend:
# Better Auth Configuration
BETTER_AUTH_SECRET=your-secret-key
BETTER_AUTH_URL=http://localhost:3001

# Apple Sign-In (iOS)
APPLE_CLIENT_ID=your.app.bundle.id
APPLE_TEAM_ID=YOUR_TEAM_ID
APPLE_KEY_ID=YOUR_KEY_ID
APPLE_PRIVATE_KEY_BASE64=base64_encoded_private_key

# Database
DATABASE_URL=postgresql://user:password@localhost:5432/launch

Next Steps

See the Environment Setup Guide for complete configuration details.

Next Steps