better-auth-expo

Better Auth - Expo/React Native Integration

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "better-auth-expo" with this command: npx skills add 5dlabs/cto/5dlabs-cto-better-auth-expo

Better Auth - Expo/React Native Integration

Better Auth provides first-class Expo support via the @better-auth/expo plugin for secure mobile authentication.

AI Tooling

IMPORTANT: Before implementing Better Auth in Expo, consult:

Use Context7 to look up Better Auth Expo patterns:

get_library_docs({ libraryName: "better-auth", topic: "expo integration" }) get_library_docs({ libraryName: "better-auth", topic: "expo social sign-in" }) get_library_docs({ libraryName: "better-auth", topic: "expo secure store" })

Installation

Server dependencies (backend)

npx expo install better-auth @better-auth/expo

Client dependencies (Expo app)

npx expo install better-auth @better-auth/expo expo-secure-store expo-linking expo-web-browser expo-constants

Environment Variables

.env (backend)

BETTER_AUTH_SECRET=your-secret-key-at-least-32-chars BETTER_AUTH_URL=http://localhost:8081

Server Configuration

Backend (lib/auth.ts ):

import { betterAuth } from "better-auth" import { expo } from "@better-auth/expo" import { drizzleAdapter } from "better-auth/adapters/drizzle" import { db } from "@/db"

export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg" }), plugins: [expo()], // Enable Expo support emailAndPassword: { enabled: true, }, socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }, apple: { clientId: process.env.APPLE_CLIENT_ID!, clientSecret: process.env.APPLE_CLIENT_SECRET!, }, }, // Trust your app's deep link scheme trustedOrigins: [ "myapp://", // Development mode (Expo's exp:// scheme) ...(process.env.NODE_ENV === "development" ? [ "exp://", "exp://**", ] : []), ], })

Expo API Route (app/api/auth/[...auth]+api.ts ):

import { auth } from "@/lib/auth"

const handler = auth.handler export { handler as GET, handler as POST }

Client Configuration

Auth Client (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_URL || "http://localhost:8081", plugins: [ expoClient({ scheme: "myapp", // Must match app.json scheme storagePrefix: "myapp", // Prefix for secure storage keys storage: SecureStore, // Secure credential storage }), ], })

export const { signIn, signUp, signOut, useSession } = authClient

App Configuration

app.json :

{ "expo": { "scheme": "myapp", "plugins": [ "expo-router", "expo-secure-store" ] } }

Metro Config (metro.config.js ):

const { getDefaultConfig } = require("expo/metro-config")

const config = getDefaultConfig(__dirname) config.resolver.unstable_enablePackageExports = true // Required for Better Auth

module.exports = config

Sign In Component

import { useState } from "react" import { View, TextInput, Button, Text, StyleSheet } from "react-native" import { authClient } from "@/lib/auth-client" import { router } from "expo-router"

export default function SignIn() { const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [error, setError] = useState<string | null>(null) const [loading, setLoading] = useState(false)

const handleSignIn = async () => { setLoading(true) setError(null)

const { error } = await authClient.signIn.email({
  email,
  password,
  callbackURL: "/dashboard",  // Converts to myapp://dashboard
})

if (error) {
  setError(error.message)
} else {
  router.replace("/dashboard")
}
setLoading(false)

}

return ( <View style={styles.container}> <TextInput style={styles.input} placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" keyboardType="email-address" /> <TextInput style={styles.input} placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry /> {error && <Text style={styles.error}>{error}</Text>} <Button title={loading ? "Signing in..." : "Sign In"} onPress={handleSignIn} disabled={loading} /> </View> ) }

const styles = StyleSheet.create({ container: { flex: 1, padding: 20, justifyContent: "center" }, input: { borderWidth: 1, borderColor: "#ccc", padding: 12, marginBottom: 12, borderRadius: 8 }, error: { color: "red", marginBottom: 12 }, })

Social Sign-In

import { Button, View } from "react-native" import { authClient } from "@/lib/auth-client"

export default function SocialSignIn() { const handleGoogleSignIn = async () => { await authClient.signIn.social({ provider: "google", callbackURL: "/dashboard", // Deep links to myapp://dashboard }) }

const handleAppleSignIn = async () => { await authClient.signIn.social({ provider: "apple", callbackURL: "/dashboard", }) }

return ( <View style={{ gap: 12 }}> <Button title="Continue with Google" onPress={handleGoogleSignIn} /> <Button title="Continue with Apple" onPress={handleAppleSignIn} /> </View> ) }

IdToken Sign-In (Native Provider SDKs):

// Use when authenticating via native Google/Apple SDKs await authClient.signIn.social({ provider: "google", idToken: { token: "id-token-from-native-sdk", nonce: "optional-nonce", }, callbackURL: "/dashboard", })

Session Hook

import { View, Text, Button, ActivityIndicator } from "react-native" import { authClient } from "@/lib/auth-client" import { router } from "expo-router"

export default function Profile() { const { data: session, isPending } = authClient.useSession()

if (isPending) { return <ActivityIndicator /> }

if (!session) { router.replace("/sign-in") return null }

const handleSignOut = async () => { await authClient.signOut() router.replace("/sign-in") }

return ( <View style={{ flex: 1, padding: 20 }}> <Text style={{ fontSize: 24, fontWeight: "bold" }}> Welcome, {session.user.name}! </Text> <Text style={{ color: "#666", marginTop: 8 }}> {session.user.email} </Text> <Button title="Sign Out" onPress={handleSignOut} /> </View> ) }

Authenticated API Requests

Better Auth stores session cookies in SecureStore. For authenticated API requests:

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

async function fetchProtectedData() { const cookies = authClient.getCookie() // Get session cookies

const response = await fetch("http://localhost:8081/api/protected", { headers: { Cookie: cookies, }, credentials: "omit", // Don't let fetch manage cookies })

return response.json() }

With tRPC:

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

const trpcClient = api.createClient({ links: [ httpBatchLink({ url: ${API_URL}/trpc, headers() { const cookies = authClient.getCookie() return cookies ? { Cookie: cookies } : {} }, }), ], })

Protected Routes with Expo Router

// app/(auth)/_layout.tsx import { Redirect, Stack } from "expo-router" import { authClient } from "@/lib/auth-client" import { ActivityIndicator, View } from "react-native"

export default function AuthLayout() { const { data: session, isPending } = authClient.useSession()

if (isPending) { return ( <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <ActivityIndicator size="large" /> </View> ) }

if (!session) { return <Redirect href="/sign-in" /> }

return <Stack /> }

Best Practices

  • Always use SecureStore for credential storage on mobile

  • Configure trustedOrigins with your app scheme for deep linking

  • Enable unstable_enablePackageExports in Metro config

  • Use native provider SDKs (Google Sign-In, Apple Sign-In) with idToken for best UX

  • Cache sessions - Better Auth caches in SecureStore automatically

  • Handle offline gracefully - sessions persist across app restarts

  • Clear cache on logout - authClient.signOut() handles this

Documentation: https://better-auth.com/docs/integrations/expo

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

expo-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

elysia-llm-docs

No summary provided by upstream source.

Repository SourceNeeds Review
General

context7

No summary provided by upstream source.

Repository SourceNeeds Review
General

bun-llm-docs

No summary provided by upstream source.

Repository SourceNeeds Review