10+ AI SaaS templates for web & mobile
home

Better Auth with Expo: production React Native setup (2026)

·13 min read

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 AuthFollow the sections below in order
Greenfield Expo + webSkim mobile auth overview, then implement
Clerk todayMigration guide, then add Expo
Auth screens only on webBetter 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/expo

In 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-network

For 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 --clear

React 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:

packages/auth/src/server.ts
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://",
  ],
});

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):

app.json
{
  "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.

apps/mobile/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, // 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.
  • storage must 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. Set disableCache: true on expoClient if 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:

Sign-in screen (excerpt)
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.

Redirect-based social login calls authClient.signIn.social. Pass a relative callbackURL; the Expo plugin turns it into a deep link (myapp://dashboard):

Google OAuth (redirect flow)
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:

Authenticated fetch
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.

ApproachWorks on Expo?Notes
credentials: "include" onlyUnreliableNo shared cookie jar with API host
authClient.getCookie() + Cookie headerYesOfficial pattern
Bearer token custom layerYesExtra 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

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.

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

Sounds good?Now let's make it real. In minutes.
Try TurboStarter

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.

world map
Community

Connect with like-minded people

Join our community to get feedback, support, and grow together with 1,000+ builders on board, let's ship it!

Join us

Ship your startup everywhere. In minutes.

Don't burn tokens on setup and start building features on day one.

Get TurboStarter