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 to1.3.34fixed 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
trustedOriginsmust include your Expo scheme, otherwise auth callbacks won’t work.- The Google
promptoption 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 🚀