Better Auth with Expo: production React Native setup (2026)
Better Auth Expo guide: @better-auth/expo, SecureStore sessions, trustedOrigins, deep-link OAuth, and getCookie for API calls on one Next.js backend.

Search traffic for better auth expo is small but real (about 90 US searches per month with almost no competition, plus better auth react native at roughly 40). Google Search Console for turbostarter.dev already shows dozens of impressions on page one for those queries, but almost no clicks, because the SERP is thin: GitHub issues, Reddit threads, and Expo docs, not a single end-to-end production guide. This post is that guide.
You are not choosing a new auth library. You already picked Better Auth for your Next.js API. The job now is to make the same sessions work in an Expo app: @better-auth/expo on the server, expoClient plus SecureStore on the client, deep-link OAuth, and getCookie() on every authenticated fetch. If you are still comparing vendors, read Better Auth vs Clerk vs NextAuth vs Supabase Auth first. For web-only UI, see Better Auth UI. For Clerk cutover, see migrate from Clerk to Better Auth.
Short answer
Better Auth works with Expo through the official @better-auth/expo package (as of October 2026, documented for Expo SDK 55 with the New Architecture). On your auth server, install better-auth and @better-auth/expo, add the expo() plugin, and list your app scheme in trustedOrigins (for example myapp://). In the Expo app, install the same packages plus expo-secure-store and expo-network, create the client from better-auth/react with expoClient({ scheme, storage: SecureStore }), and send session cookies to your API with authClient.getCookie(). Social sign-in uses deep links back to your scheme; email/password uses the same client methods as web. One Better Auth backend can serve Next.js and Expo from the same user tables.
Who this guide is for
This is the dedicated Better Auth Expo setup path. It does not repeat our native mobile apps for web developers Expo primer or the full cross-platform SaaS monorepo architecture essay. It goes deep on auth wiring only.
| You have… | Start here |
|---|---|
| Next.js API + Postgres already on Better Auth | Follow the sections below in order |
| Greenfield Expo + web | Skim mobile auth overview, then implement |
| Clerk today | Migration guide, then add Expo |
| Auth screens only on web | Better Auth UI plus this post for mobile |
Install @better-auth/expo (server and client)
Better Auth splits Expo support into a server plugin and a client plugin. Install both sides into the projects that need them.
On the auth server (Next.js route handler, Hono mount, or Expo API Routes host):
pnpm add better-auth @better-auth/expoIn the Expo app (and the same packages if you colocate a tiny API route in Expo):
pnpm add better-auth @better-auth/expo expo-secure-store expo-networkFor redirect-based social login in the system browser, add expo-linking, expo-web-browser, and expo-constants if your template does not already include them.
The official integration guide targets Expo SDK 55 (React Native 0.83, React 19.2). SDK 55 requires the New Architecture; plan upgrades before you treat Legacy Architecture workarounds as long-term. Metro must keep package exports enabled (default on SDK 53+). If auth modules fail to resolve, clear the bundler cache:
npx expo start --clearReact Native without Expo
Better auth react native searches often mean Expo, because @better-auth/expo is the supported path. Bare React Native can call the same HTTP auth API, but you lose the maintained SecureStore and deep-link helpers. Unless you have a hard requirement to avoid Expo, use the official Expo integration and treat bare RN as a custom client problem.
Server: expo() plugin and trustedOrigins
Your web app already exposes Better Auth at something like /api/auth/*. Mobile does not change the database or session model. It changes how cookies are stored and how OAuth returns to the app.
Add the server plugin next to your other Better Auth plugins:
import { betterAuth } from "better-auth";
import { expo } from "@better-auth/expo";
export const auth = betterAuth({
plugins: [
expo(),
// ...magicLink, nextCookies, etc.
],
emailAndPassword: { enabled: true },
trustedOrigins: [
"https://your-production-domain.com",
"myapp://",
],
});Why trustedOrigins matters for deep-link OAuth
Better Auth uses deep links to hand control back to your app after OAuth or magic-link flows. The origin must be explicit. Define scheme in app.json (or app.config.ts):
{
"expo": {
"scheme": "myapp"
}
}Then allow myapp:// in trustedOrigins. Multiple builds (staging, prod) mean multiple entries:
trustedOrigins: [
"myapp://",
"myapp-staging://",
"myapp-prod://",
"myapp://*",
],Development: exp:// and local IPs
In development, Expo serves over exp:// with your machine IP. Trust those patterns only in development:
trustedOrigins: [
"myapp://",
...(process.env.NODE_ENV === "development"
? ["exp://", "exp://**", "exp://192.168.*.*:*/**"]
: []),
],Shipping exp:// wildcards to production is an open redirect risk. Keep them behind NODE_ENV === "development".
TurboStarter lists the mobile scheme (for example turbostarter://) in shared auth config so web and mobile stay aligned. See mobile auth configuration for where that lives and how to rename the scheme safely.
Client: expoClient and SecureStore sessions
Mobile has no shared cookie jar with your API host. The Expo client plugin stores session cookies in SecureStore and attaches them to Better Auth requests.
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, // e.g. https://api.example.com/api/auth
plugins: [
expoClient({
scheme: "myapp",
storagePrefix: "myapp",
storage: SecureStore,
}),
],
});Important details from the official docs:
- Import the React client from
better-auth/react, not a separate native entry. - Pass the full
baseURL, including the path if you changed the default/api/auth. storagemust be SecureStore-compatible. Reuse the same storage instance if you split clients.useSession()can read cached session data from SecureStore on cold start so you skip a flash of logged-out UI. SetdisableCache: trueonexpoClientif you prefer always hitting the network.
cookiePrefix when you customize server cookies
If your server sets advanced.cookiePrefix (TurboStarter uses a project-specific prefix), tell the client which cookie names belong to Better Auth:
expoClient({
storage: SecureStore,
cookiePrefix: ["better-auth", "turbostarter-dev"],
}),Without the right prefix, the client may ignore session cookies or refetch session in a loop when third-party cookies appear in the jar.
Email and password in Expo
Once the client is wired, credential flows match the web client API:
import { useState } from "react";
import { View, TextInput, Button } from "react-native";
import { authClient } from "@/lib/auth-client";
export function SignIn() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const onSubmit = async () => {
const { error } = await authClient.signIn.email({ email, password });
if (error) {
// surface error.message
return;
}
// navigate to your protected stack
};
return (
<View>
<TextInput value={email} onChangeText={setEmail} autoCapitalize="none" />
<TextInput value={password} onChangeText={setPassword} secureTextEntry />
<Button title="Sign in" onPress={onSubmit} />
</View>
);
}Sign-up uses authClient.signUp.email({ email, password, name }). Magic links and OTP follow the same pattern as web once deep linking is configured (mobile deep linking). For 2FA after sign-in, wire the same Better Auth plugins as web and follow mobile 2FA.
Social OAuth and deep links
Redirect-based social login calls authClient.signIn.social. Pass a relative callbackURL; the Expo plugin turns it into a deep link (myapp://dashboard):
import { Button } from "react-native";
import { router } from "expo-router";
import { authClient } from "@/lib/auth-client";
export function GoogleSignIn() {
const handlePress = async () => {
const { error } = await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
if (error) return;
router.replace("/dashboard");
};
return <Button title="Continue with Google" onPress={handlePress} />;
}On native iOS and Android, signIn.social does not navigate for you. Handle routing after the promise resolves. The in-app browser behavior differs by platform; test both.
Native idToken sign-in (Google, Apple, Facebook)
When you already have a provider idToken from @react-native-google-signin/google-signin or Sign in with Apple, skip the browser redirect:
await authClient.signIn.social({
provider: "google",
idToken: { token: idTokenFromSdk },
callbackURL: "/dashboard",
});TurboStarter documents native Apple and Google paths under mobile OAuth and provider-specific guides for Apple and Google. Provider setup still happens in the Apple and Google consoles; Better Auth verifies tokens on your server.
OAuth redirect gotchas
If OAuth succeeds in the browser but the app never receives the session, check scheme, trustedOrigins, and that production builds use the production scheme. A mismatch between expoClient({ scheme }) and app.json is the most common fix. Second place: forgetting development exp:// origins while testing in Expo Go.
Calling your API with getCookie
Better Auth session cookies live in SecureStore, not in fetch credentials. For your API routes (REST, tRPC, Hono RPC), read cookies and set the header yourself:
import { authClient } from "@/lib/auth-client";
export async function fetchDashboard() {
const cookies = await authClient.getCookie();
const res = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/dashboard`, {
headers: cookies ? { Cookie: cookies } : {},
credentials: "omit", // do not fight manual Cookie header
});
return res.json();
}Use the same pattern in tRPC headers(), React Query fetchers, or any client that talks to the same origin as Better Auth.
| Approach | Works on Expo? | Notes |
|---|---|---|
credentials: "include" only | Unreliable | No shared cookie jar with API host |
authClient.getCookie() + Cookie header | Yes | Official pattern |
| Bearer token custom layer | Yes | Extra work; not default Better Auth |
This is the difference between "signed in on the auth screen" and "401 on every API call". Our cross-platform backend guide describes how TurboStarter's typed mobile client sends the cookie and platform headers together.
Web + Expo on one Better Auth backend
The ownership model is simple: one user table, one session table, one auth config. Next.js hosts the handler; Expo is another client.
┌─────────────────────┐
│ Postgres (users, │
│ sessions, accounts)│
└──────────▲──────────┘
│
┌──────────┴──────────┐
│ Better Auth server │
│ + expo() plugin │
└──────────▲──────────┘
┌───────────────────┼───────────────────┐
│ │ │
Next.js cookies SecureStore + Extension
(browser) getCookie() (separate doc)Benefits you actually feel in production:
- A user who signs up on web can sign in on mobile with the same email or OAuth provider.
- Admin actions and bans apply everywhere without a sync job.
- You own the schema. No per-MAU auth tax as mobile grows.
Tradeoff vs Clerk Expo: Clerk ships hosted components and Expo Go shortcuts. Better Auth ships control: your tables, your plugins, your deep-link and SecureStore wiring. Clerk can be faster on day one; Better Auth tends to win when you already run Postgres and want web and mobile on one identity layer. We compared models in the auth comparison post.
Ready-made layout: Expo Better Auth mobile app template and React Native Better Auth starter kit. The full TurboStarter Core Kit ships Next.js, Expo, and extension clients on one API.
Common Expo + Better Auth bugs
You forgot getCookie() on API requests, or you used credentials: "include" while also setting Cookie. Use manual Cookie from authClient.getCookie() and credentials: "omit".
Check trustedOrigins includes your scheme (myapp://). In dev, add exp:// patterns. Confirm expoClient({ scheme }) matches app.json.
SecureStore has size limits per value. Very large cookie payloads can fail silently in edge cases. Confirm you are on a dev build or release build, not a misconfigured web-only target. Clear app data once after changing storagePrefix.
Wrong cookiePrefix on expoClient when the server uses a custom advanced.cookiePrefix. Align prefixes with the server config.
Package exports must stay enabled. Run npx expo start --clear. On SDK 55, do not disable unstable_enablePackageExports in Metro.
If you host Better Auth inside Expo API Routes, the server plugin supports disableOriginOverride for Expo-specific origin issues. Prefer a dedicated Next.js API in production if you already have one.
Ship checklist
Server
Install @better-auth/expo, add expo() to plugins, enable the auth methods you need, and list app schemes plus site URLs in trustedOrigins.
Expo app
Install client deps, configure expoClient with SecureStore, set EXPO_PUBLIC_API_URL to your auth base URL, and define scheme in app config.
OAuth and deep links
Configure providers on the server, test redirect and native idToken flows on a real device, and verify callback deep links open the correct screen.
API layer
Thread getCookie() through your data client. Confirm protected routes return 200 when the session is valid.
Production hardening
Remove development exp:// origins, use HTTPS API URLs, and align cookiePrefix on client and server. Add mobile security practices from TurboStarter docs.
FAQ
Install better-auth and @better-auth/expo on the server and client, add expo() and expoClient({ storage: SecureStore }), set trustedOrigins to your app scheme, and use authClient.getCookie() for API calls. Follow the official Expo integration.
There is no first-party bare-RN plugin like @better-auth/expo. You can implement cookie storage and OAuth yourself, but Expo is the supported path documented as of October 2026.
In expo-secure-store by default, via the expoClient plugin. Session data can be cached for faster cold starts unless you set disableCache: true.
The server completes OAuth and redirects to your registered scheme (myapp://...). Better Auth validates the origin against trustedOrigins. The client plugin completes the flow and persists cookies to SecureStore.
Yes, when both talk to the same Better Auth server and database. Web uses browser cookies; mobile uses SecureStore plus getCookie() on API requests. The user record is shared; only the storage mechanism differs.
The official guide documents Expo SDK 55 (New Architecture). Older SDKs may work with adjustments, but plan to match the documented SDK when you greenfield a app in late 2026.
Better auth expo is not a separate product. It is Better Auth with the Expo plugin, SecureStore, trusted deep-link origins, and explicit cookies on API calls. Wire those four pieces on top of the server you already run for Next.js, and you get production-ready better auth react native sessions without a second user database. If you want that monorepo already connected, TurboStarter ships Expo, web, and extension on one Better Auth backend so you can ship the app instead of debugging cookie headers for a month.
Best SaaS starter kit 2026: 10 picks (Next.js, TanStack, Expo)
Compare 10 SaaS starter kits for 2026 on stack, pricing, teams, mobile, and Cloudflare support, with a clear pick for each type of product you build.
Better Auth UI: the complete Next.js + shadcn guide (2026)
What Better Auth UI is, how to install it with shadcn, wire AuthProvider in Next.js, protect routes, and when a full SaaS starter beats DIY auth screens.



