
better-auth-expo
by 5dlabs
Cognitive Task Orchestrator - GitOps on Bare Metal or Cloud for AI Agents
SKILL.md
name: better-auth-expo description: Better Auth integration for Expo/React Native with SecureStore, deep linking, and native provider SDKs agents: [tap] triggers: [auth, login, expo, mobile, securestore, deep link] context7_libraries:
- /better-auth/better-auth llm_docs:
- better-auth
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
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon


