Docs

Expo Integration

Expo is a popular framework for building cross-platform apps with React Native. Better Auth supports both Expo native and web apps.

Installation

Configure A Better Auth Backend

Before using Better Auth with Expo, make sure you have a Better Auth backend set up. You can either use a separate server or leverage Expo's new API Routes feature to host your Better Auth instance.

To get started, check out our installation guide for setting up Better Auth on your server. If you prefer to check out the full example, you can find it here.

To use the new API routes feature in Expo to host your Better Auth instance you can create a new API route in your Expo app and mount the Better Auth handler.

app/api/auth/[...auth]+api.ts
import { auth } from "@/lib/auth"; // import Better Auth handler
 
const handler = auth.handler;
export { handler as GET, handler as POST }; // export handler for both GET and POST requests

Install Better Auth and Expo Plugin

Install both the expo plugin in your server and both the Better Auth package and the Expo plugin in your Expo app.

npm install @better-auth/expo
npm install better-auth

Add the Expo Plugin on Your Server

Add the Expo plugin to your Better Auth server.

server.ts
import { betterAuth } from "better-auth";
import { expo } from "@better-auth/expo";
 
export const auth = betterAuth({
    plugins: [
        expo()
    ]
});

Initialize Better Auth Client

To initialize Better Auth in your Expo app, you need to call createAuthClient with the base url of your Better Auth backend. Make sure to import the client from /react.

You need to also import client plugin from @better-auth/expo/client and pass it to the plugins array when initializing the auth client.

This is important because:

  • Social Authentication Support: enables social auth flows by handling authorization URLs and callbacks within the Expo web browser.
  • Secure Cookie Management: stores cookies securely and automatically adds them to the headers of your auth requests.
src/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: "http://localhost:8081", /* base url of your Better Auth backend. */
    plugins: [
        expoClient({
            scheme: "myapp",
            storagePrefix: "myapp",
            storage: SecureStore,
        })
    ]
});

Be sure to include the full URL, including the path, if you've changed the default path from /api/auth.

Scheme and Trusted Origins

Better Auth uses deep links to redirect users back to your app after authentication. To enable this, you need to add your app's scheme to the trustedOrigins list in your Better Auth config.

First, make sure you have a scheme defined in your app.json file.

app.json
{
    "expo": {
        "scheme": "myapp"
    }
}

Then, update your Better Auth config to include the scheme in the trustedOrigins list.

auth.ts
export const auth = betterAuth({
    trustedOrigins: ["myapp://"]
})

Configure Metro Bundler

To resolve better auth exports you'll need to enable unstable_enablePackageExports in your metro config.

metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
 
const config = getDefaultConfig(__dirname)
 
config.resolver.unstable_enablePackageExports = true; 
 
module.exports = config;

If you can't enable unstable_enablePackageExports option, you can use babel-plugin-module-resolver to manually resolve the paths.

babel.config.js
module.exports = function (api) {
    return {
        plugins: [
            [
                "module-resolver",
                {
                    alias: {
                        "better-auth/react": "path/to/node_modules/better-auth/dist/react.js"
                        "@better-auth/expo/client": "path/to/node_modules/@better-auth/expo/dist/client.js",
                    },
                    extensions: [".js", ".jsx", ".ts", ".tsx"],
                },
            ],
        ],
    }
}

Don't forget to clear the cache after making changes.

npx expo start --clear

Usage

Authenticating Users

With Better Auth initialized, you can now use the authClient to authenticate users in your Expo app.

app/sign-in.tsx
import { useState } from "react"; 
import { View, TextInput, Button } from "react-native";
import { authClient } from "./auth-client";
 
export default function App() {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
 
    const handleLogin = async () => {
    await authClient.signIn.email({
            email,
            password,
    })
    };
 
    return (
        <View>
            <TextInput
                placeholder="Email"
                value={email}
                onChangeText={setEmail}
            />
            <TextInput
                placeholder="Password"
                value={password}
                onChangeText={setPassword}
            />
            <Button title="Login" onPress={handleLogin} />
        </View>
    );
}

For social sign-in, you can use the authClient.signIn.social method with the provider name and a callback URL.

app/social-sign-in.tsx
 
import { Button } from "react-native";
 
export default function App() {
    const handleLogin = async () => {
        await authClient.signIn.social({
            provider: "google",
            callbackURL: "/dashboard" // this will be converted to a deep link (eg. `myapp://dashboard`) on native
        })
    };
    return <Button title="Login with Google" onPress={handleLogin} />;
}

Session

Better Auth provides a useSession hook to access the current user's session in your app.

src/App.tsx
 
import { authClient } from "@/lib/auth-client";
 
export default function App() {
    const { data: session } = authClient.useSession();
 
    return <Text>Welcome, {data.user.name}</Text>;
}

On native, the session data will be cached in SecureStore. This will allow you to remove the need for a loading spinner when the app is reloaded. You can disable this behavior by passing the disableCache option to the client.

Making Authenticated Requests to Your Server

To make authenticated requests to your server that require the user's session, you have two options:

  1. Use the fetch client provided by Better Auth.
  2. Retrieve the session cookie from SecureStore and manually add it to your request headers.

Option 1: Using the Fetch Client

Better Auth provides a built-in fetch client powered by Better Fetch. This client automatically includes the session cookie in the headers of your requests.

import { authClient } from "@/lib/auth-client";
 
const $fetch = authClient.$fetch;
 
// Example usage
$fetch("/api/secure-endpoint", {
  method: "GET",
});

For more details, see the Better Fetch documentation.

If you prefer using your own fetch client, you can retrieve the session cookie stored in the device using authClient.getCookie and manually add it to your request headers.

import { authClient } from "@/lib/auth-client";
 
const makeAuthenticatedRequest = async () => {
  const cookies = authClient.getCookie(); 
  const headers = {
    "Cookie": cookies, 
  };
  const response = await fetch("http://localhost:8081/api/secure-endpoint", { headers });
  const data = await response.json();
  return data;
};

Example: Usage With TRPC

lib/trpc-provider.tsx
//...other imports
import { authClient } from "@/lib/auth-client"; 
 
export const api = createTRPCReact<AppRouter>();
 
export function TRPCProvider(props: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    api.createClient({
      links: [
        httpBatchLink({
          //...your other options
          headers() {
            const headers = new Map<string, string>(); 
            const cookies = authClient.getCookie(); 
            if (cookies) { 
              headers.set("Cookie", cookies); 
            } 
            return Object.fromEntries(headers); 
          },
        }),
      ],
    }),
  );
 
  return (
    <api.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {props.children}
      </QueryClientProvider>
    </api.Provider>
  );
}

Options

Expo Client

storage: the storage mechanism used to cache the session data and cookies.

src/auth-client.ts
import { createAuthClient } from "better-auth/react";
import SecureStorage from "expo-secure-store";
 
const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    storage: SecureStorage
});

scheme: scheme is used to deep link back to your app after a user has authenticated using oAuth providers. By default, Better Auth tries to read the scheme from the app.json file. If you need to override this, you can pass the scheme option to the client.

src/auth-client.ts
import { createAuthClient } from "better-auth/react";
 
const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    scheme: "myapp"
});

disableCache: By default, the client will cache the session data in SecureStore. You can disable this behavior by passing the disableCache option to the client.

src/auth-client.ts
import { createAuthClient } from "better-auth/react";
 
const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    disableCache: true
});

Expo Servers

Server plugin options:

overrideOrigin: Override the origin for expo API routes (default: false). Enable this if you're facing cors origin issues with expo API routes.