How to setup auth in Expo with Better Auth and Payload CMS

A step-by-step guide to setting up authentication in Expo using Better Auth and Payload CMS, including version pitfalls, trusted origins, and working client configuration.

This took way longer than expected, so I’m writing this in the hope it saves someone else a few hours (or days).

I recently set up authentication for an Expo app using Payload CMS, Payload-Auth, and Better Auth. On the server side, things were pretty smooth. Payload-Auth makes it easy to wire Better Auth into Payload.

Where things got tricky was getting the Better Auth client working correctly in Expo. Version mismatches, Expo schemes, trusted origins, and storage behavior all came into play.

Here’s the setup that finally worked.


The Stack

  • Next.js + Payload CMS
  • Payload-Auth (powered by Better Auth)
  • Expo (React Native)
  • Google social login + email/password auth

Versions That Worked (Important)

Version compatibility mattered a lot here. These are the exact versions I used.

Server (Next.js + Payload CMS)

{
  "@better-auth/expo": "1.4.3",
  "better-auth": "1.3.34",
  "payload": "3.64.0",
  "payload-auth": "1.7.1"
}

⚠️ I ran into issues with better-auth@1.4.3. Downgrading to 1.3.34 fixed them.


Payload CMS + Payload-Auth Configuration

In your payload.config.ts, configure the Better Auth plugin like this:

betterAuthPlugin({
  // Add your Expo app scheme to trusted origins
  trustedOrigins: ["exp://"],

  // Add the Expo plugin from @better-auth/expo
  plugins: [expo()],

  // Google social sign-in config
  // The prompt option is important, otherwise Google
  // may auto-login without showing account selection
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
      prompt: "select_account consent",
    },
  },
});

Key Notes

  • trustedOrigins must include your Expo scheme, otherwise auth callbacks won’t work.
  • The Google prompt option is critical if you want users to choose an account instead of being auto-signed in.

Expo App Setup

Expo Package Versions

{
  "@better-auth/expo": "1.3.7",
  "better-auth": "1.3.7",
  "expo": "54.0.1",
  "expo-secure-store": "15.0.7"
}

Yes — the Expo client uses older versions than the server. This was intentional and avoided several runtime issues.


Creating the Auth Client in Expo

Create a shared auth client (for example, lib/auth-client.ts).

import * as SecureStore from "expo-secure-store";
import { createAuthClient } from "better-auth";
import { expoClient } from "@better-auth/expo";

export const authClient = createAuthClient({
  // Your site URL (Payload / Next.js)
  // Locally this is usually http://localhost:3000
  baseURL: "http://localhost:3000",

  // Required for Expo
  disableDefaultFetchPlugins: true,

  plugins: [
    expoClient({
      scheme: "exp://",
      storagePrefix: "betterauth",
      storage: SecureStore,
    }),
  ],
});

Why This Matters

  • Expo does not behave like the web, so you must:

    • Disable default fetch plugins
    • Use expoClient
    • Persist auth data using expo-secure-store

Checking if a User Is Authenticated

You can check session state anywhere in your app:

const { data } = authClient.useSession();
const isAuthenticated = !!data?.session;

This makes it easy to:

  • Gate screens
  • Show login vs logged-in UI
  • Handle auth state consistently

Signing In

Google Social Login

await authClient.signIn.social({
  provider: "google",
  callbackURL: "/",
});

Email + Password

await authClient.signIn.email({
  email,
  password,
});

Both methods worked reliably once the versions and Expo config were aligned.


Final Thoughts

Payload-Auth + Better Auth is a solid setup, but Expo adds complexity that isn’t immediately obvious:

  • Version mismatches can silently break things
  • Trusted origins and schemes are critical
  • Secure storage must be configured explicitly

Once everything is aligned, though, auth works smoothly across web and mobile.

Hopefully this saves you the same debugging rabbit hole I went down. If you’re trying to run Payload CMS as a unified auth backend for web + Expo, this setup gets you there.

Happy shipping 🚀

All posts