[GH-ISSUE #3180] Better Auth cookies caching from Expo app (client) to Next JS API (server) #9503

Closed
opened 2026-04-13 04:59:20 -05:00 by GiteaMirror · 11 comments
Owner

Originally created by @quuentinho on GitHub (Jun 26, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3180

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

To reproduce

Step 1: Setup Better Auth with Expo + Next.js

// lib/auth/auth.js
import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { nextCookies } from "better-auth/next-js"
import { admin as adminPlugin, apiKey, customSession } from "better-auth/plugins"
import { expo } from "@better-auth/expo"

export const auth = betterAuth({
    database: drizzleAdapter(db, {
        provider: "pg",
        schema: { /* your schema */ }
    }),
    trustedOrigins: [
        "http://localhost:3000",
        "http://app.localhost",
        "myapp://*"
    ],
    session: {
        cookieCache: {
            maxAge: 7 * 24 * 60 * 60, // 7 days
            updateAge: 1 * 24 * 60 * 60, // 1 day
        }
    },
    plugins: [
        adminPlugin({
            adminRoles: ["ROLE_ADMIN"],
            defaultRole: ["ROLE_USER"],
        }),
        customSession(async ({ user, session }) => {
            // Additional user info from DB
            const additionalInfo = await getAdditionalSessionInfo(session.userId)
            return {
                user: { ...user, ...additionalInfo },
                session,
            }
        }),
        apiKey(),
        expo(),
        nextCookies(), // Last in array
    ],
})

Step 2: Create server-side auth helper

// lib/auth/auth-server.js
import { auth } from "./auth"

export async function requireAuth(headers) {
    const session = await auth.api.getSession({ 
        headers, 
        query: { disableCookieCache: true } 
    })

    if (!session) {
        throw new Error("Unauthorized")
    }
    
    return { session, user }
}

Step 3: Create Next.js API route

// app/api/mobile/areas/route.js
import { requireAuth } from "@/lib/auth/auth-server"

export async function GET(request) {
    try {
        const { session, user } = await requireAuth(request.headers)
        
        // Your API logic here
        return Response.json({ areas: [] })
    } catch (error) {
        return Response.json({ error: error.message }, { status: 401 })
    }
}

Step 4: Setup Expo app with Better Auth

// In your Expo app
import { createAuthClient } from "better-auth/react"

const authClient = createAuthClient({
    baseURL: "http://192.168.2.118:3000" // Your Next.js server
})

// Login user
await authClient.signIn.email({
    email: "user@example.com", 
    password: "password"
})

_______________________________________

const cookie = authClient.getCookie()

// Make authenticated request
const response = await fetch('http://192.168.2.118:3000/api/mobile/areas', {
    method: 'GET',
    headers: {
        'Cookie': cookie
    }
})

Step 5: Observe the issue

  1. User successfully logs in - session is created and visible in Expo app
  2. Make API request from Expo app to Next.js API route
  3. API returns 401 Unauthorized despite valid session

Current vs. Expected behavior

Current vs. Expected behavior

Current behavior ( Bug)

Client-side (Expo app):

  • User login succeeds
  • Session object is properly returned with user data
  • Cookies are visible in client logs:
    better-auth.session_token=xxx.yyy%3D
    better-auth.session_data=eyJ...base64...
    

Server-side (Next.js API):

  • Better Auth cookies are completely missing from request headers
  • auth.api.getSession() returns null
  • API returns 401 Unauthorized

Server logs:

All headers:
accept: application/json, text/plain, */*
cookie: XXXXXXX  // ❌ No better-auth cookies!
user-agent: okhttp/4.9.2

Session found: false
GET /api/mobile/test 401 Unauthorized

Expected behavior ( What should happen)

Server-side (Next.js API):

  • Better Auth cookies should be included in request headers:
    cookie: better-auth.session_token=xxx.yyy; better-auth.session_data=eyJ...
    
  • auth.api.getSession() should return valid session object
  • API should return successful response with data

The core issue: Better Auth session cookies are being sent by the client but never reaching the server, while other cookies work fine.

Additional Context

What I've tried:

  1. Adding credentials: 'include' to all fetch requests
  2. Configuring CORS with credentials in Better Auth config
  3. Using disableCookieCache: true in getSession
  4. Temporarily removing customSession plugin
  5. Verifying trustedOrigins includes correct URLs
  6. Testing with different Next.js API route patterns

Environment details:

  • Development setup: Docker containers
  • Network: Local network (192.168.x.x)
  • Client IP: 192.168.2.35 (Expo app)
  • Server IP: 192.168.2.118:3000 (Next.js)
  • Issue frequency: Intermittent - sometimes works, then suddenly stops

Suspicious observations:

  • Sessions are created in database but expire immediately
  • Issue seems related to expo() plugin or customSession interaction
  • Works initially after login (and i have my current session) then breaks on subsequent requests

Package versions:

{
  "better-auth": "^1.2.8",
  "@better-auth/expo": "^1.2.8",
  "next": "15.3.3",
  "expo": "~52.0.0",
  "react-native": "0.76.0"
}

This appears to be a cookie transmission issue specific to the Better Auth + Expo + Next.js combination. Any guidance on proper configuration or known workarounds would be greatly appreciated! 🙏

What version of Better Auth are you using?

1.2.10

Provide environment information

- OS : Ubuntu 24.10
- Expo SDK 53 (development build)
- Next JS 15.3.3

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @quuentinho on GitHub (Jun 26, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3180 ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce ## To reproduce ### Step 1: Setup Better Auth with Expo + Next.js ```js // lib/auth/auth.js import { betterAuth } from "better-auth" import { drizzleAdapter } from "better-auth/adapters/drizzle" import { nextCookies } from "better-auth/next-js" import { admin as adminPlugin, apiKey, customSession } from "better-auth/plugins" import { expo } from "@better-auth/expo" export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema: { /* your schema */ } }), trustedOrigins: [ "http://localhost:3000", "http://app.localhost", "myapp://*" ], session: { cookieCache: { maxAge: 7 * 24 * 60 * 60, // 7 days updateAge: 1 * 24 * 60 * 60, // 1 day } }, plugins: [ adminPlugin({ adminRoles: ["ROLE_ADMIN"], defaultRole: ["ROLE_USER"], }), customSession(async ({ user, session }) => { // Additional user info from DB const additionalInfo = await getAdditionalSessionInfo(session.userId) return { user: { ...user, ...additionalInfo }, session, } }), apiKey(), expo(), nextCookies(), // Last in array ], }) ``` ### Step 2: Create server-side auth helper ```js // lib/auth/auth-server.js import { auth } from "./auth" export async function requireAuth(headers) { const session = await auth.api.getSession({ headers, query: { disableCookieCache: true } }) if (!session) { throw new Error("Unauthorized") } return { session, user } } ``` ### Step 3: Create Next.js API route ```js // app/api/mobile/areas/route.js import { requireAuth } from "@/lib/auth/auth-server" export async function GET(request) { try { const { session, user } = await requireAuth(request.headers) // Your API logic here return Response.json({ areas: [] }) } catch (error) { return Response.json({ error: error.message }, { status: 401 }) } } ``` ### Step 4: Setup Expo app with Better Auth ```js // In your Expo app import { createAuthClient } from "better-auth/react" const authClient = createAuthClient({ baseURL: "http://192.168.2.118:3000" // Your Next.js server }) // Login user await authClient.signIn.email({ email: "user@example.com", password: "password" }) _______________________________________ const cookie = authClient.getCookie() // Make authenticated request const response = await fetch('http://192.168.2.118:3000/api/mobile/areas', { method: 'GET', headers: { 'Cookie': cookie } }) ``` ### Step 5: Observe the issue 1. **User successfully logs in** - session is created and visible in Expo app 2. **Make API request** from Expo app to Next.js API route 3. **API returns 401 Unauthorized** despite valid session ### Current vs. Expected behavior ## Current vs. Expected behavior ### Current behavior (❌ Bug) **Client-side (Expo app):** - ✅ User login succeeds - ✅ Session object is properly returned with user data - ✅ Cookies are visible in client logs: ``` better-auth.session_token=xxx.yyy%3D better-auth.session_data=eyJ...base64... ``` **Server-side (Next.js API):** - ❌ Better Auth cookies are completely missing from request headers - ❌ `auth.api.getSession()` returns `null` - ❌ API returns 401 Unauthorized **Server logs:** ``` All headers: accept: application/json, text/plain, */* cookie: XXXXXXX // ❌ No better-auth cookies! user-agent: okhttp/4.9.2 Session found: false GET /api/mobile/test 401 Unauthorized ``` ### Expected behavior (✅ What should happen) **Server-side (Next.js API):** - ✅ Better Auth cookies should be included in request headers: ``` cookie: better-auth.session_token=xxx.yyy; better-auth.session_data=eyJ... ``` - ✅ `auth.api.getSession()` should return valid session object - ✅ API should return successful response with data **The core issue:** Better Auth session cookies are being **sent by the client** but **never reaching the server**, while other cookies work fine. ## Additional Context ### What I've tried: 1. ✅ **Adding `credentials: 'include'` to all fetch requests** 2. ✅ **Configuring CORS with credentials in Better Auth config** 3. ✅ **Using `disableCookieCache: true` in getSession** 4. ✅ **Temporarily removing `customSession` plugin** 6. ✅ **Verifying `trustedOrigins` includes correct URLs** 7. ✅ **Testing with different Next.js API route patterns** ### Environment details: - **Development setup:** Docker containers - **Network:** Local network (192.168.x.x) - **Client IP:** 192.168.2.35 (Expo app) - **Server IP:** 192.168.2.118:3000 (Next.js) - **Issue frequency:** Intermittent - sometimes works, then suddenly stops ### Suspicious observations: - Sessions are created in database but expire immediately - Issue seems related to `expo()` plugin or `customSession` interaction - Works initially after login (and i have my current session) then breaks on subsequent requests ## Package versions: ```json { "better-auth": "^1.2.8", "@better-auth/expo": "^1.2.8", "next": "15.3.3", "expo": "~52.0.0", "react-native": "0.76.0" } ``` This appears to be a cookie transmission issue specific to the Better Auth + Expo + Next.js combination. Any guidance on proper configuration or known workarounds would be greatly appreciated! 🙏 ### What version of Better Auth are you using? 1.2.10 ### Provide environment information ```bash - OS : Ubuntu 24.10 - Expo SDK 53 (development build) - Next JS 15.3.3 ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-13 04:59:20 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jun 26, 2025):

The root cause is a set of bugs in the @better-auth/expo client plugin’s cookie expiration logic: it parses expires as a string (not a Date), treats max-age as milliseconds (should be seconds), and gives Expires precedence over Max-Age (should be the opposite). This causes cookies to expire immediately or be considered invalid, so your Expo app sends what looks like valid cookies, but the server ignores them as expired or missing, resulting in 401s—even though you see the cookie string in your logs. This is a known issue and a patch is available for @better-auth/expo@1.2.9+ that fixes all three problems. You can see the patch and the required code changes here:
How to fix cookie expiration parsing in @better-auth/expo?

Make sure you are using the latest @better-auth/expo version with this fix, or apply the patch if you’re on 1.2.9. If you’re using a custom session, also update to the latest better-auth core, as there was a recent fix for cookie caching with custom sessions (fix/cookie cache with custom session).

For best results, use the expoClient plugin from @better-auth/expo/client in your Expo app and pass it to the plugins array when initializing your auth client. Also, install and use expo-secure-store for secure cookie storage. On the server, keep expo() and nextCookies() in your plugin list (with nextCookies() last), and enable the overrideOrigin option in the Expo plugin if you run into CORS issues (docs).

If you still see issues after patching, double-check that your cookies are not expired (log the cookie object and check the expires field), and that the cookie string sent in the request matches what the server expects. Also, try temporarily disabling your custom session logic to rule out session invalidation.

Let me know if you need the patch code or want help verifying your setup.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

<!-- gh-comment-id:3008650014 --> @dosubot[bot] commented on GitHub (Jun 26, 2025): <!-- Answer --> The root cause is a set of bugs in the @better-auth/expo client plugin’s cookie expiration logic: it parses `expires` as a string (not a Date), treats `max-age` as milliseconds (should be seconds), and gives `Expires` precedence over `Max-Age` (should be the opposite). This causes cookies to expire immediately or be considered invalid, so your Expo app sends what looks like valid cookies, but the server ignores them as expired or missing, resulting in 401s—even though you see the cookie string in your logs. This is a known issue and a patch is available for @better-auth/expo@1.2.9+ that fixes all three problems. You can see the patch and the required code changes here: [How to fix cookie expiration parsing in @better-auth/expo?](https://github.com/better-auth/better-auth/issues/3034) Make sure you are using the latest @better-auth/expo version with this fix, or apply the patch if you’re on 1.2.9. If you’re using a custom session, also update to the latest better-auth core, as there was a recent fix for cookie caching with custom sessions ([fix/cookie cache with custom session](https://github.com/better-auth/better-auth/pull/3156)). For best results, use the `expoClient` plugin from `@better-auth/expo/client` in your Expo app and pass it to the `plugins` array when initializing your auth client. Also, install and use `expo-secure-store` for secure cookie storage. On the server, keep `expo()` and `nextCookies()` in your plugin list (with `nextCookies()` last), and enable the `overrideOrigin` option in the Expo plugin if you run into CORS issues ([docs](https://github.com/better-auth/better-auth/pull/2335)). If you still see issues after patching, double-check that your cookies are not expired (log the cookie object and check the `expires` field), and that the cookie string sent in the request matches what the server expects. Also, try temporarily disabling your custom session logic to rule out session invalidation. Let me know if you need the patch code or want help verifying your setup. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3180)
Author
Owner

@quuentinho commented on GitHub (Jun 26, 2025):

The root cause is a set of bugs in the @better-auth/expo client plugin’s cookie expiration logic: it parses expires as a string (not a Date), treats max-age as milliseconds (should be seconds), and gives Expires precedence over Max-Age (should be the opposite). This causes cookies to expire immediately or be considered invalid, so your Expo app sends what looks like valid cookies, but the server ignores them as expired or missing, resulting in 401s—even though you see the cookie string in your logs. This is a known issue and a patch is available for @better-auth/expo@1.2.9+ that fixes all three problems. You can see the patch and the required code changes here: How to fix cookie expiration parsing in @better-auth/expo?

Make sure you are using the latest @better-auth/expo version with this fix, or apply the patch if you’re on 1.2.9. If you’re using a custom session, also update to the latest better-auth core, as there was a recent fix for cookie caching with custom sessions (fix/cookie cache with custom session).

For best results, use the expoClient plugin from @better-auth/expo/client in your Expo app and pass it to the plugins array when initializing your auth client. Also, install and use expo-secure-store for secure cookie storage. On the server, keep expo() and nextCookies() in your plugin list (with nextCookies() last), and enable the overrideOrigin option in the Expo plugin if you run into CORS issues (docs).

If you still see issues after patching, double-check that your cookies are not expired (log the cookie object and check the expires field), and that the cookie string sent in the request matches what the server expects. Also, try temporarily disabling your custom session logic to rule out session invalidation.

Let me know if you need the patch code or want help verifying your setup.

To reply, just mention @dosu.

How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

Even if I update my packages to the most recent versions, I still have the same problem.

LOG cookie: {"better-auth.session_token":{"value":"ATDqY40YBNkewYOG1XuSF2meR20N9Cce.CBh3PO1sHCsC0pyQ8JCnG4XVMBBGdIyiftoQKp%2FVuLI%3D","expires":"2025-06-26T15:55:16.837Z"},"better-auth.session_data":{"value":"eyJzZXNzaW9uIjp7InNlc3Npb24iOnsiZXhwaXJlc0F0IjoiMjAyNS0wNy0wM1QxNTo0NToxMi4wMDlaIiwidG9rZW4iOiJBVERxWTQwWUJOa2V3WU9HMVh1U0YybWVSMjBOOUNjZSIsImNyZWF0ZWRBdCI6IjIwMjUtMDYtMjZUMTU6NDU6MTIuMDEwWiIsInVwZGF0ZWRBdCI6IjIwMjUtMDYtMjZUMTU6NDU6MTIuMDEwWiIsImlwQWRkcmVzcyI6IjE5Mi4xNjguMi4zNSIsInVzZXJBZ2VudCI6Im9raHR0cC80LjkuMiIsInVzZXJJZCI6ImRmY2M2MzgxLTY5ZDgtNDMxYy1hMWJkLWEyOWFlNTVhYmUwNiIsImltcGVyc29uYXRlZEJ5IjpudWxsLCJpZCI6IjZmMTcwNzU2LWFkYWEtNGZhZi1iMWZiLWIzMThhOTYzMjgxYiJ9LCJ1c2VyIjp7Im5hbWUiOiJQYXJpcyBBZG1pbiIsImVtYWlsIjoicGFyaXNfYWRtaW5AY29udGFjdC5jb20iLCJlbWFpbFZlcmlmaWVkIjp0cnVlLCJpbWFnZSI6bnVsbCwiY3JlYXRlZEF0IjoiMjAyNS0wNi0yNVQxNTozNToxNy4zODRaIiwidXBkYXRlZEF0IjoiMjAyNS0wNi0yNVQxNTozNToxNy4zODFaIiwicm9sZSI6IlJPTEVfVVNFUixST0xFX0FETUlOIiwiYmFubmVkIjpudWxsLCJiYW5SZWFzb24iOm51bGwsImJhbkV4cGlyZXMiOm51bGwsImlkIjoiZGZjYzYzODEtNjlkOC00MzFjLWExYmQtYTI5YWU1NWFiZTA2In19LCJleHBpcmVzQXQiOjE3NTE1NTc1MTIwMTksInNpZ25hdHVyZSI6IkhXRVYyd3Fta3dDQ3NJM25EdmluUExlM3pJM1lNQjBqTmRrMjhYUEh0QUEifQ","expires":"2025-06-26T15:55:16.837Z"},"better-auth.dont_remember":{"value":"","expires":"2025-06-26T15:45:08.701Z"}}

LOG session_data: {"session":{"expiresAt":"2025-07-03T15:45:12.009Z","token":"ATDqY40YBNkewYOG1XuSF2meR20N9Cce","createdAt":"2025-06-26T15:45:12.010Z","updatedAt":"2025-06-26T15:45:12.010Z","ipAddress":"192.168.2.35","userAgent":"okhttp/4.9.2","userId":"dfcc6381-69d8-431c-a1bd-a29ae55abe06","impersonatedBy":null,"id":"6f170756-adaa-4faf-b1fb-b318a963281b"},"user":{"name":"User Admin","email":"user_admin@contact.com","emailVerified":true,"image":null,"createdAt":"2025-06-25T15:35:17.384Z","updatedAt":"2025-06-25T15:35:17.381Z","role":"ROLE_USER,ROLE_ADMIN","banned":null,"banReason":null,"banExpires":null,"id":"dfcc6381-69d8-431c-a1bd-a29ae55abe06"}}

<!-- gh-comment-id:3008975380 --> @quuentinho commented on GitHub (Jun 26, 2025): > The root cause is a set of bugs in the @better-auth/expo client plugin’s cookie expiration logic: it parses `expires` as a string (not a Date), treats `max-age` as milliseconds (should be seconds), and gives `Expires` precedence over `Max-Age` (should be the opposite). This causes cookies to expire immediately or be considered invalid, so your Expo app sends what looks like valid cookies, but the server ignores them as expired or missing, resulting in 401s—even though you see the cookie string in your logs. This is a known issue and a patch is available for @better-auth/expo@1.2.9+ that fixes all three problems. You can see the patch and the required code changes here: [How to fix cookie expiration parsing in @better-auth/expo?](https://github.com/better-auth/better-auth/issues/3034) > > Make sure you are using the latest @better-auth/expo version with this fix, or apply the patch if you’re on 1.2.9. If you’re using a custom session, also update to the latest better-auth core, as there was a recent fix for cookie caching with custom sessions ([fix/cookie cache with custom session](https://github.com/better-auth/better-auth/pull/3156)). > > For best results, use the `expoClient` plugin from `@better-auth/expo/client` in your Expo app and pass it to the `plugins` array when initializing your auth client. Also, install and use `expo-secure-store` for secure cookie storage. On the server, keep `expo()` and `nextCookies()` in your plugin list (with `nextCookies()` last), and enable the `overrideOrigin` option in the Expo plugin if you run into CORS issues ([docs](https://github.com/better-auth/better-auth/pull/2335)). > > If you still see issues after patching, double-check that your cookies are not expired (log the cookie object and check the `expires` field), and that the cookie string sent in the request matches what the server expects. Also, try temporarily disabling your custom session logic to rule out session invalidation. > > Let me know if you need the patch code or want help verifying your setup. > > _To reply, just mention [@dosu](https://go.dosu.dev/dosubot)._ > > How did I do? [Good](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/90cea88b-efc1-4293-bdae-12e8b086c193?feedback_type=other)  [![Join Discord](https://camo.githubusercontent.com/d562c2df5e4567ea41d37f1d91a4da0839d8341037045b103b2da268998de17f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a6f696e2d3538363546323f6c6f676f3d646973636f7264266c6f676f436f6c6f723d7768697465266c6162656c3d)](https://go.dosu.dev/discord-bot) [![Share on X](https://camo.githubusercontent.com/4649f705e2396d6f328dbc8e800a0509c9c17b3b20253b36706c393df124b93d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f582d73686172652d626c61636b)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3180) Even if I update my packages to the most recent versions, I still have the same problem. LOG cookie: {"better-auth.session_token":{"value":"ATDqY40YBNkewYOG1XuSF2meR20N9Cce.CBh3PO1sHCsC0pyQ8JCnG4XVMBBGdIyiftoQKp%2FVuLI%3D","expires":"2025-06-26T15:55:16.837Z"},"better-auth.session_data":{"value":"eyJzZXNzaW9uIjp7InNlc3Npb24iOnsiZXhwaXJlc0F0IjoiMjAyNS0wNy0wM1QxNTo0NToxMi4wMDlaIiwidG9rZW4iOiJBVERxWTQwWUJOa2V3WU9HMVh1U0YybWVSMjBOOUNjZSIsImNyZWF0ZWRBdCI6IjIwMjUtMDYtMjZUMTU6NDU6MTIuMDEwWiIsInVwZGF0ZWRBdCI6IjIwMjUtMDYtMjZUMTU6NDU6MTIuMDEwWiIsImlwQWRkcmVzcyI6IjE5Mi4xNjguMi4zNSIsInVzZXJBZ2VudCI6Im9raHR0cC80LjkuMiIsInVzZXJJZCI6ImRmY2M2MzgxLTY5ZDgtNDMxYy1hMWJkLWEyOWFlNTVhYmUwNiIsImltcGVyc29uYXRlZEJ5IjpudWxsLCJpZCI6IjZmMTcwNzU2LWFkYWEtNGZhZi1iMWZiLWIzMThhOTYzMjgxYiJ9LCJ1c2VyIjp7Im5hbWUiOiJQYXJpcyBBZG1pbiIsImVtYWlsIjoicGFyaXNfYWRtaW5AY29udGFjdC5jb20iLCJlbWFpbFZlcmlmaWVkIjp0cnVlLCJpbWFnZSI6bnVsbCwiY3JlYXRlZEF0IjoiMjAyNS0wNi0yNVQxNTozNToxNy4zODRaIiwidXBkYXRlZEF0IjoiMjAyNS0wNi0yNVQxNTozNToxNy4zODFaIiwicm9sZSI6IlJPTEVfVVNFUixST0xFX0FETUlOIiwiYmFubmVkIjpudWxsLCJiYW5SZWFzb24iOm51bGwsImJhbkV4cGlyZXMiOm51bGwsImlkIjoiZGZjYzYzODEtNjlkOC00MzFjLWExYmQtYTI5YWU1NWFiZTA2In19LCJleHBpcmVzQXQiOjE3NTE1NTc1MTIwMTksInNpZ25hdHVyZSI6IkhXRVYyd3Fta3dDQ3NJM25EdmluUExlM3pJM1lNQjBqTmRrMjhYUEh0QUEifQ","expires":"2025-06-26T15:55:16.837Z"},"better-auth.dont_remember":{"value":"","expires":"2025-06-26T15:45:08.701Z"}} LOG session_data: {"session":{"expiresAt":"2025-07-03T15:45:12.009Z","token":"ATDqY40YBNkewYOG1XuSF2meR20N9Cce","createdAt":"2025-06-26T15:45:12.010Z","updatedAt":"2025-06-26T15:45:12.010Z","ipAddress":"192.168.2.35","userAgent":"okhttp/4.9.2","userId":"dfcc6381-69d8-431c-a1bd-a29ae55abe06","impersonatedBy":null,"id":"6f170756-adaa-4faf-b1fb-b318a963281b"},"user":{"name":"User Admin","email":"[user_admin@contact.com](mailto:user_admin@contact.com)","emailVerified":true,"image":null,"createdAt":"2025-06-25T15:35:17.384Z","updatedAt":"2025-06-25T15:35:17.381Z","role":"ROLE_USER,ROLE_ADMIN","banned":null,"banReason":null,"banExpires":null,"id":"dfcc6381-69d8-431c-a1bd-a29ae55abe06"}}
Author
Owner

@dosubot[bot] commented on GitHub (Jun 26, 2025):

If you’re still seeing 401s after updating to the latest versions and confirming the patch, but your client logs show valid, unexpired cookies, the most likely causes are:

  1. The cookies are not actually reaching the Next.js server (they may be set in the fetch request, but stripped or altered by Docker, a reverse proxy, or CORS/network config).
  2. There’s a mismatch in cookie names, prefixes, or secure flags between client and server config, so the server ignores the cookies even if present.
  3. The customSession plugin or another plugin is mutating the session object or context, causing validation to fail.
  4. The trustedOrigins or origin handling is not matching what the server expects for Expo requests.

Here’s what to check next:

  • Log and inspect the raw incoming request headers (especially the Cookie header) on your Next.js API route. If the Better Auth cookies are missing, the issue is in the network/proxy layer or fetch config.
  • If the cookies are present but still rejected, double-check that your cookiePrefix, secureCookiePrefix, and cookie names match exactly between client and server (docs).
  • Make sure you’re using the expoClient plugin from @better-auth/expo/client with SecureStore in your Expo app, and that expo() and nextCookies() are both present on the server, with nextCookies() last (docs).
  • Try disabling customSession and any non-essential plugins to see if the session is then accepted.
  • If you’re running behind Docker or a reverse proxy, ensure headers are forwarded unmodified and that CORS allows credentials and matches the client’s domain (source).
  • If you use custom domains or schemes, make sure trustedOrigins includes the correct values (e.g., exp://, myapp://*).

If you can share the raw incoming request headers from the server, that will help pinpoint whether the cookies are being stripped or just not recognized.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

<!-- gh-comment-id:3008999887 --> @dosubot[bot] commented on GitHub (Jun 26, 2025): <!-- Answer --> If you’re still seeing 401s after updating to the latest versions and confirming the patch, but your client logs show valid, unexpired cookies, the most likely causes are: 1. The cookies are not actually reaching the Next.js server (they may be set in the fetch request, but stripped or altered by Docker, a reverse proxy, or CORS/network config). 2. There’s a mismatch in cookie names, prefixes, or secure flags between client and server config, so the server ignores the cookies even if present. 3. The customSession plugin or another plugin is mutating the session object or context, causing validation to fail. 4. The trustedOrigins or origin handling is not matching what the server expects for Expo requests. Here’s what to check next: - Log and inspect the raw incoming request headers (especially the Cookie header) on your Next.js API route. If the Better Auth cookies are missing, the issue is in the network/proxy layer or fetch config. - If the cookies are present but still rejected, double-check that your cookiePrefix, secureCookiePrefix, and cookie names match exactly between client and server ([docs](https://better-auth.com/docs/integrations/next)). - Make sure you’re using the expoClient plugin from @better-auth/expo/client with SecureStore in your Expo app, and that expo() and nextCookies() are both present on the server, with nextCookies() last ([docs](https://better-auth.com/docs/integrations/expo)). - Try disabling customSession and any non-essential plugins to see if the session is then accepted. - If you’re running behind Docker or a reverse proxy, ensure headers are forwarded unmodified and that CORS allows credentials and matches the client’s domain ([source](https://github.com/better-auth/better-auth/pull/2209)). - If you use custom domains or schemes, make sure trustedOrigins includes the correct values (e.g., exp://, myapp://*). If you can share the raw incoming request headers from the server, that will help pinpoint whether the cookies are being stripped or just not recognized. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3180)
Author
Owner

@quuentinho commented on GitHub (Jun 26, 2025):

Si vous voyez toujours des erreurs 401 après la mise à jour vers les dernières versions et la confirmation du correctif, mais que vos journaux clients affichent des cookies valides et non expirés, les causes les plus probables sont :

  1. Les cookies n'atteignent pas réellement le serveur Next.js (ils peuvent être définis dans la demande de récupération, mais supprimés ou modifiés par Docker, un proxy inverse ou une configuration CORS/réseau).
  2. Il existe une incompatibilité dans les noms de cookies, les préfixes ou les indicateurs de sécurité entre la configuration du client et celle du serveur, de sorte que le serveur ignore les cookies même s'ils sont présents.
  3. Le plugin customSession ou un autre plugin modifie l'objet ou le contexte de session, ce qui entraîne l'échec de la validation.
  4. La gestion des origines ou de l'origine de confiance ne correspond pas à ce que le serveur attend pour les requêtes Expo.

Voici ce qu’il faut vérifier ensuite :

  • Enregistrez et inspectez les en-têtes bruts des requêtes entrantes (en particulier l'en-tête Cookie) sur votre route API Next.js. Si les cookies Better Auth sont manquants, le problème se situe au niveau de la couche réseau/proxy ou de la configuration de récupération.
  • Si les cookies sont présents mais toujours rejetés, vérifiez que vos noms cookiePrefix, secureCookiePrefix et cookie correspondent exactement entre le client et le serveur ( docs ).
  • Assurez-vous que vous utilisez le plugin expoClient de @better-auth/expo/client avec SecureStore dans votre application Expo, et que expo() et nextCookies() sont tous deux présents sur le serveur, avec nextCookies() en dernier ( docs ).
  • Essayez de désactiver customSession et tous les plugins non essentiels pour voir si la session est ensuite acceptée.
  • Si vous utilisez Docker ou un proxy inverse, assurez-vous que les en-têtes sont transmis sans modification et que CORS autorise les informations d'identification et correspond au domaine du client ( source ).
  • Si vous utilisez des domaines ou des schémas personnalisés, assurez-vous que trustedOrigins inclut les valeurs correctes (par exemple, exp://, myapp://*).

Si vous pouvez partager les en-têtes de requête entrants bruts du serveur, cela aidera à déterminer si les cookies sont supprimés ou simplement non reconnus.

Pour répondre, mentionnez simplement @dosu .

Comment ai-je fait ? Bien | Hors sujet | Incorrect | Verbeux | Hallucination | Signaler 🐛 | Autre  Rejoignez Discord Partager sur X

The cookies of incoming requests are deleted (better auth token...):

request Request {
test-1 | method: 'GET',
test-1 | url: 'http://container:3000/api/test',
test-1 | headers: Headers {
test-1 | accept: 'application/json, text/plain, /',
test-1 | host: '192.168.2.118:3000',
test-1 | connection: 'Keep-Alive',
test-1 | 'accept-encoding': 'gzip',
test-1 | cookie: [others cookies, not better auth]
test-1 | 'user-agent': 'okhttp/4.9.2',
test-1 | 'if-modified-since': 'Thu, 26 Jun 2025 15:27:26 GMT',
test-1 | 'x-forwarded-host': '192.168.2.118:3000',
test-1 | 'x-forwarded-port': '3000',
test-1 | 'x-forwarded-proto': 'http',
test-1 | 'x-forwarded-for': '192.168.2.35'
test-1 | },
test-1 | destination: '',
test-1 | referrer: 'about:client',
test-1 | referrerPolicy: '',
test-1 | mode: 'cors',
test-1 | credentials: 'same-origin',
test-1 | cache: 'default',
test-1 | redirect: 'follow',
test-1 | integrity: '',
test-1 | keepalive: false,
test-1 | isReloadNavigation: false,
test-1 | isHistoryNavigation: false,
test-1 | signal: AbortSignal { aborted: false }
test-1 | }

<!-- gh-comment-id:3009019267 --> @quuentinho commented on GitHub (Jun 26, 2025): > Si vous voyez toujours des erreurs 401 après la mise à jour vers les dernières versions et la confirmation du correctif, mais que vos journaux clients affichent des cookies valides et non expirés, les causes les plus probables sont : > > 1. Les cookies n'atteignent pas réellement le serveur Next.js (ils peuvent être définis dans la demande de récupération, mais supprimés ou modifiés par Docker, un proxy inverse ou une configuration CORS/réseau). > 2. Il existe une incompatibilité dans les noms de cookies, les préfixes ou les indicateurs de sécurité entre la configuration du client et celle du serveur, de sorte que le serveur ignore les cookies même s'ils sont présents. > 3. Le plugin customSession ou un autre plugin modifie l'objet ou le contexte de session, ce qui entraîne l'échec de la validation. > 4. La gestion des origines ou de l'origine de confiance ne correspond pas à ce que le serveur attend pour les requêtes Expo. > > Voici ce qu’il faut vérifier ensuite : > > * Enregistrez et inspectez les en-têtes bruts des requêtes entrantes (en particulier l'en-tête Cookie) sur votre route API Next.js. Si les cookies Better Auth sont manquants, le problème se situe au niveau de la couche réseau/proxy ou de la configuration de récupération. > * Si les cookies sont présents mais toujours rejetés, vérifiez que vos noms cookiePrefix, secureCookiePrefix et cookie correspondent exactement entre le client et le serveur ( [docs](https://better-auth.com/docs/integrations/next) ). > * Assurez-vous que vous utilisez le plugin expoClient de @better-auth/expo/client avec SecureStore dans votre application Expo, et que expo() et nextCookies() sont tous deux présents sur le serveur, avec nextCookies() en dernier ( [docs](https://better-auth.com/docs/integrations/expo) ). > * Essayez de désactiver customSession et tous les plugins non essentiels pour voir si la session est ensuite acceptée. > * Si vous utilisez Docker ou un proxy inverse, assurez-vous que les en-têtes sont transmis sans modification et que CORS autorise les informations d'identification et correspond au domaine du client ( [source](https://github.com/better-auth/better-auth/pull/2209) ). > * Si vous utilisez des domaines ou des schémas personnalisés, assurez-vous que trustedOrigins inclut les valeurs correctes (par exemple, exp://, myapp://*). > > Si vous pouvez partager les en-têtes de requête entrants bruts du serveur, cela aidera à déterminer si les cookies sont supprimés ou simplement non reconnus. > > _Pour répondre, mentionnez simplement [@dosu](https://go.dosu.dev/dosubot) ._ > > Comment ai-je fait ? [Bien](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=great_response) | Hors [sujet](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=too_verbose) | [Incorrect](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=irrelevant_answer) | [Verbeux](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=incorrect_sources) | [Hallucination](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=hallucination) | [Signaler 🐛](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=bug_report) | [Autre](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=other)[](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=too_verbose)[](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=hallucination)[](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=bug_report)[](https://app.dosu.dev/response-feedback/5b6156cc-4f57-46e4-829e-bc561711c950?feedback_type=other)  [![Rejoignez Discord](https://camo.githubusercontent.com/d562c2df5e4567ea41d37f1d91a4da0839d8341037045b103b2da268998de17f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a6f696e2d3538363546323f6c6f676f3d646973636f7264266c6f676f436f6c6f723d7768697465266c6162656c3d)](https://go.dosu.dev/discord-bot) [![Partager sur X](https://camo.githubusercontent.com/4649f705e2396d6f328dbc8e800a0509c9c17b3b20253b36706c393df124b93d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f582d73686172652d626c61636b)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3180) The cookies of incoming requests are deleted (better auth token...): request Request { test-1 | method: 'GET', test-1 | url: 'http://container:3000/api/test', test-1 | headers: Headers { test-1 | accept: 'application/json, text/plain, */*', test-1 | host: '192.168.2.118:3000', test-1 | connection: 'Keep-Alive', test-1 | 'accept-encoding': 'gzip', test-1 | cookie: [others cookies, not better auth] test-1 | 'user-agent': 'okhttp/4.9.2', test-1 | 'if-modified-since': 'Thu, 26 Jun 2025 15:27:26 GMT', test-1 | 'x-forwarded-host': '192.168.2.118:3000', test-1 | 'x-forwarded-port': '3000', test-1 | 'x-forwarded-proto': 'http', test-1 | 'x-forwarded-for': '192.168.2.35' test-1 | }, test-1 | destination: '', test-1 | referrer: 'about:client', test-1 | referrerPolicy: '', test-1 | mode: 'cors', test-1 | credentials: 'same-origin', test-1 | cache: 'default', test-1 | redirect: 'follow', test-1 | integrity: '', test-1 | keepalive: false, test-1 | isReloadNavigation: false, test-1 | isHistoryNavigation: false, test-1 | signal: AbortSignal { aborted: false } test-1 | }
Author
Owner

@Lyons800 commented on GitHub (Jul 7, 2025):

Having the same problem. Any update on this issue?

<!-- gh-comment-id:3044784961 --> @Lyons800 commented on GitHub (Jul 7, 2025): Having the same problem. Any update on this issue?
Author
Owner

@GabrielMarcC commented on GitHub (Jul 15, 2025):

@QuentinBoulay the same issue here, do you find any solution?

<!-- gh-comment-id:3075379894 --> @GabrielMarcC commented on GitHub (Jul 15, 2025): @QuentinBoulay the same issue here, do you find any solution?
Author
Owner

@tonychandesign commented on GitHub (Jul 20, 2025):

same issue here

const { data: session } = authClient.useSession();

session is also null

but for some reason the cookie is thre

const cookie = authClient.getCookie();
console.log(cookie);
LOG ; __Secure-better-auth.session_token="SOMETOKEN"

<!-- gh-comment-id:3092640032 --> @tonychandesign commented on GitHub (Jul 20, 2025): same issue here const { data: session } = authClient.useSession(); session is also null but for some reason the cookie is thre const cookie = authClient.getCookie(); console.log(cookie); LOG ; __Secure-better-auth.session_token="SOMETOKEN"
Author
Owner

@edukinara commented on GitHub (Jul 27, 2025):

Adding credentials: 'omit' to fetch options appears to fix this for me.

<!-- gh-comment-id:3123682275 --> @edukinara commented on GitHub (Jul 27, 2025): Adding `credentials: 'omit'` to fetch options appears to fix this for me.
Author
Owner

@Kinfe123 commented on GitHub (Aug 11, 2025):

Can you update to the latest and check if the issue exists ?

<!-- gh-comment-id:3176869211 --> @Kinfe123 commented on GitHub (Aug 11, 2025): Can you update to the latest and check if the issue exists ?
Author
Owner

@ingokpp commented on GitHub (Oct 20, 2025):

I ran into a similar issue with v1.3.28 + Expo. Two problems I discovered:

  1. cookiePrefix must match between client and server

If your backend uses a custom cookiePrefix:

// Backend

betterAuth({
  advanced: {
    cookiePrefix: "myapp"
  }
})

Your client must match it exactly:

// Client

expoClient({
  cookiePrefix: "myapp"  // Must match backend!
})

Otherwise the Expo plugin filters out your cookies and never stores them.

2. Storage keys use colons which SecureStore rejects <- This is a bug

Better Auth creates keys like myapp:cookie but SecureStore only allows alphanumeric, ., -, _. This causes silent storage failures.

I fixed this by creating a wrapper around secure-storage:

import * as SecureStore from 'expo-secure-store';

/**
 * SecureStore adapter for Better Auth
 *
 * SecureStore doesn't allow colons in key names, but Better Auth uses
 * keys like "prefix:keyName". This adapter replaces colons with underscores.
 *
 * Valid SecureStore key characters: alphanumeric, ".", "-", "_"
 *
 * NOTE: Better Auth expects synchronous storage, but SecureStore is async.
 * This adapter uses an in-memory cache to provide sync access.
 */

const normalizeKey = (key: string): string => {
  // Replace colons with underscores to make keys compatible with SecureStore
  const normalized = key.replace(/:/g, '_');
  return normalized;
};

// In-memory cache for sync access
// Security Note: This cache stores tokens in plain memory for performance.
// The cache is:
// - Isolated to app's memory space (other apps cannot access)
// - Backed up to encrypted SecureStore
// - Cleared on logout (via removeItem)
// This is a recommended pattern for React Native auth tokens.
const cache = new Map<string, string | null>();

// Initialize cache from SecureStore on startup
const STORAGE_PREFIX = 'cookieprefix'; // Must match storagePrefix in auth-client
const COOKIE_KEY = `${STORAGE_PREFIX}_cookie`;
const SESSION_DATA_KEY = `${STORAGE_PREFIX}_session_data`;

// Pre-load critical keys into cache
(async () => {
  console.log('🔧 [SecureStore Adapter] Pre-loading cache...');
  try {
    const cookie = await SecureStore.getItemAsync(COOKIE_KEY);
    const sessionData = await SecureStore.getItemAsync(SESSION_DATA_KEY);

    if (cookie) {
      cache.set(COOKIE_KEY, cookie);
      console.log('✅ [SecureStore Adapter] Loaded cookie into cache');
    }
    if (sessionData) {
      cache.set(SESSION_DATA_KEY, sessionData);
      console.log('✅ [SecureStore Adapter] Loaded session_data into cache');
    }
  } catch (error) {
    console.error('❌ [SecureStore Adapter] Error pre-loading cache:', error);
  }
})();

export const secureStoreAdapter = {
  // Sync method that reads from cache (Better Auth expects sync)
  getItem: (key: string): string | null => {
    const normalizedKey = normalizeKey(key);
    const cachedValue = cache.get(normalizedKey) ?? null;

    console.log(`🔑 [SecureStore Adapter] getItem (sync) ${key}:`, cachedValue ? `${cachedValue.length} chars (from cache)` : 'null');

    return cachedValue;
  },

  // Sync method that writes to cache AND async to SecureStore
  setItem: (key: string, value: string): void => {
    const normalizedKey = normalizeKey(key);
    console.log(`🔑 [SecureStore Adapter] setItem (sync) ${key}: ${value.length} chars`);

    // Update cache immediately (sync)
    cache.set(normalizedKey, value);

    // Persist to SecureStore in background (async)
    SecureStore.setItemAsync(normalizedKey, value)
      .then(() => {
        console.log(`✅ [SecureStore Adapter] Persisted ${normalizedKey} to SecureStore`);
      })
      .catch(error => {
        console.error(`❌ [SecureStore Adapter] Error persisting ${normalizedKey}:`, error);
      });
  },

  // Sync method that removes from cache AND async from SecureStore
  removeItem: (key: string): void => {
    const normalizedKey = normalizeKey(key);
    console.log(`🔑 [SecureStore Adapter] removeItem (sync) ${key}`);

    // Remove from cache immediately (sync)
    cache.delete(normalizedKey);

    // Remove from SecureStore in background (async)
    SecureStore.deleteItemAsync(normalizedKey)
      .then(() => {
        console.log(`✅ [SecureStore Adapter] Removed ${normalizedKey} from SecureStore`);
      })
      .catch(error => {
        console.error(`❌ [SecureStore Adapter] Error removing ${normalizedKey}:`, error);
      });
  },
};

/**
 * Security utility: Clear all cached auth data from memory
 * Call this on logout or when auth is no longer needed
 */
export const clearAuthCache = () => {
  console.log('🔒 [SecureStore Adapter] Clearing auth cache from memory');
  cache.clear();
};

<!-- gh-comment-id:3421672118 --> @ingokpp commented on GitHub (Oct 20, 2025): I ran into a similar issue with v1.3.28 + Expo. Two problems I discovered: 1. cookiePrefix must match between client and server If your backend uses a custom cookiePrefix: // Backend ```typescript betterAuth({ advanced: { cookiePrefix: "myapp" } }) ``` Your client must match it exactly: // Client ```typescript expoClient({ cookiePrefix: "myapp" // Must match backend! }) ``` Otherwise the Expo plugin filters out your cookies and never stores them. **2. Storage keys use colons which SecureStore rejects** <- This is a bug Better Auth creates keys like myapp:cookie but SecureStore only allows alphanumeric, ., -, _. This causes silent storage failures. I fixed this by creating a wrapper around secure-storage: ```typescript import * as SecureStore from 'expo-secure-store'; /** * SecureStore adapter for Better Auth * * SecureStore doesn't allow colons in key names, but Better Auth uses * keys like "prefix:keyName". This adapter replaces colons with underscores. * * Valid SecureStore key characters: alphanumeric, ".", "-", "_" * * NOTE: Better Auth expects synchronous storage, but SecureStore is async. * This adapter uses an in-memory cache to provide sync access. */ const normalizeKey = (key: string): string => { // Replace colons with underscores to make keys compatible with SecureStore const normalized = key.replace(/:/g, '_'); return normalized; }; // In-memory cache for sync access // Security Note: This cache stores tokens in plain memory for performance. // The cache is: // - Isolated to app's memory space (other apps cannot access) // - Backed up to encrypted SecureStore // - Cleared on logout (via removeItem) // This is a recommended pattern for React Native auth tokens. const cache = new Map<string, string | null>(); // Initialize cache from SecureStore on startup const STORAGE_PREFIX = 'cookieprefix'; // Must match storagePrefix in auth-client const COOKIE_KEY = `${STORAGE_PREFIX}_cookie`; const SESSION_DATA_KEY = `${STORAGE_PREFIX}_session_data`; // Pre-load critical keys into cache (async () => { console.log('🔧 [SecureStore Adapter] Pre-loading cache...'); try { const cookie = await SecureStore.getItemAsync(COOKIE_KEY); const sessionData = await SecureStore.getItemAsync(SESSION_DATA_KEY); if (cookie) { cache.set(COOKIE_KEY, cookie); console.log('✅ [SecureStore Adapter] Loaded cookie into cache'); } if (sessionData) { cache.set(SESSION_DATA_KEY, sessionData); console.log('✅ [SecureStore Adapter] Loaded session_data into cache'); } } catch (error) { console.error('❌ [SecureStore Adapter] Error pre-loading cache:', error); } })(); export const secureStoreAdapter = { // Sync method that reads from cache (Better Auth expects sync) getItem: (key: string): string | null => { const normalizedKey = normalizeKey(key); const cachedValue = cache.get(normalizedKey) ?? null; console.log(`🔑 [SecureStore Adapter] getItem (sync) ${key}:`, cachedValue ? `${cachedValue.length} chars (from cache)` : 'null'); return cachedValue; }, // Sync method that writes to cache AND async to SecureStore setItem: (key: string, value: string): void => { const normalizedKey = normalizeKey(key); console.log(`🔑 [SecureStore Adapter] setItem (sync) ${key}: ${value.length} chars`); // Update cache immediately (sync) cache.set(normalizedKey, value); // Persist to SecureStore in background (async) SecureStore.setItemAsync(normalizedKey, value) .then(() => { console.log(`✅ [SecureStore Adapter] Persisted ${normalizedKey} to SecureStore`); }) .catch(error => { console.error(`❌ [SecureStore Adapter] Error persisting ${normalizedKey}:`, error); }); }, // Sync method that removes from cache AND async from SecureStore removeItem: (key: string): void => { const normalizedKey = normalizeKey(key); console.log(`🔑 [SecureStore Adapter] removeItem (sync) ${key}`); // Remove from cache immediately (sync) cache.delete(normalizedKey); // Remove from SecureStore in background (async) SecureStore.deleteItemAsync(normalizedKey) .then(() => { console.log(`✅ [SecureStore Adapter] Removed ${normalizedKey} from SecureStore`); }) .catch(error => { console.error(`❌ [SecureStore Adapter] Error removing ${normalizedKey}:`, error); }); }, }; /** * Security utility: Clear all cached auth data from memory * Call this on logout or when auth is no longer needed */ export const clearAuthCache = () => { console.log('🔒 [SecureStore Adapter] Clearing auth cache from memory'); cache.clear(); }; ```
Author
Owner

@ingokpp commented on GitHub (Oct 20, 2025):

See, https://github.com/better-auth/better-auth/issues/5426

<!-- gh-comment-id:3421696952 --> @ingokpp commented on GitHub (Oct 20, 2025): See, https://github.com/better-auth/better-auth/issues/5426
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9503