better-auth-expo
Better Auth integration for Expo/React Native with SecureStore, deep linking, and native provider SDKs
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
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:
-
AI Documentation:
https://better-auth.com/llms.txt -
MCP Server:
https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp
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_enablePackageExportsin 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
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-03-04
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!