auth.api.getSession() returns null despite valid session token in database and successful HTTP endpoint response #1672

Closed
opened 2026-03-13 08:55:02 -05:00 by GiteaMirror · 18 comments
Owner

Originally created by @SushiiReboot on GitHub (Aug 9, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce:

  1. Set up BetterAuth with Prisma adapter and MySQL database
  2. Configure Express server with BetterAuth routes at /api/auth/*
  3. Create a user account and sign in successfully (session created in database)
  4. Call auth.api.getSession() with valid headers containing session cookie
  5. Observe that API returns null despite valid session existing in database

Reproduction steps:

Note: Replace [SESSION] with the token that was generated upon signup.

1. Session exists in database

  docker exec freskapp-backend-db-1 mysql -u root -p freskapp_db -e "SELECT token, userId, expiresAt FROM session WHERE token = [SESSION];"
  # Returns: valid session with future expiry date

2. HTTP endpoint works and returns 200 OK but null body

  curl -X GET "http://192.168.178.134:3000/api/auth/get-session" \
    -H "Cookie: freskapp.session_token=[SESSION]" \
    -H "Origin: http://192.168.178.134:8081" -v

Returns: 200 OK with CORS headers but body is null

3. auth.api.getSession() in middleware also returns null

  const session = await auth.api.getSession({
    headers: fromNodeHeaders(req.headers)
  });

Returns: null

Current vs. Expected behavior

Current:

  • auth.api.getSession() returns null
  • HTTP endpoint /api/auth/get-session returns 200 OK with null body
  • Session token exists in database and is valid (not expired)
  • CORS headers are correctly set (Access-Control-Allow-Origin matches request origin)

Expected:

  • auth.api.getSession() should return { user: {...}, session: {...} }
  • HTTP endpoint should return the user and session data from database
  • Valid sessions in database should be recognized and returned

What version of Better Auth are you using?

1.3.4

Provide environment information

- OS: macOS
- Node.js: Latest
- Database: MySQL 8.0 (Docker)
- Server: Express.js
- Client: React Native Expo (Android emulator)
- Network: Local development (HTTP, not HTTPS)

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

Backend, Client

Auth config (if applicable)

export const auth = betterAuth({
      appName: "Freskapp",
      secret: process.env.BETTER_AUTH_SECRET,
      baseURL: "http://10.0.2.2:3000",
      database: prismaAdapter(prisma, {
          provider: "mysql"
      }),

      emailAndPassword: {
          enabled: true,
          autoSignIn: true,
          requireEmailVerification: false
      },

      plugins: [expo()],

      advanced: {
          cookiePrefix: "freskapp",
          useSecureCookies: false,
          defaultCookieAttributes: {
              sameSite: "none",
              secure: false,
              httpOnly: true,
          },
      },

      trustedOrigins: [
          "http://192.168.178.134:3000",
          "http://192.168.178.134:8081",
          "http://10.0.2.2:3000",
          "http://10.0.2.2:8081",
          "http://localhost:3000",
          "freskapp://"
      ],

      logger: {
          level: "debug",
          disabled: false
      }
  });

Additional context

  • Session creation works perfectly - users can sign up/in successfully
  • Database contains valid session data with correct expiry dates
  • CORS is working correctly (verified with verbose cURL output)
    Here's are an example with the call and the response:
     curl -X GET "http://192.168.178.134:3000/api/auth/get-session" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -H "Cookie: freskapp.session_token=vYRKSB8ZiorLLdbJUYe1WpR4P47cDTZS" \
    -H "User-Agent: Mozilla/5.0 (Android 14; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0" \
    -H "Origin: http://192.168.178.134:8081" \
    -H "Referer: http://192.168.178.134:8081/" \
    -H "X-Requested-With: XMLHttpRequest" \
    -H "Cache-Control: no-cache" \
    -v
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying 192.168.178.134:3000...
* Connected to 192.168.178.134 (192.168.178.134) port 3000
> GET /api/auth/get-session HTTP/1.1
> Host: 192.168.178.134:3000
> Content-Type: application/json
> Accept: application/json
> Cookie: freskapp.session_token=vYRKSB8ZiorLLdbJUYe1WpR4P47cDTZS
> User-Agent: Mozilla/5.0 (Android 14; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0
> Origin: http://192.168.178.134:8081
> Referer: http://192.168.178.134:8081/
> X-Requested-With: XMLHttpRequest
> Cache-Control: no-cache
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Cross-Origin-Resource-Policy: same-origin
< Referrer-Policy: no-referrer
< Strict-Transport-Security: max-age=31536000; includeSubDomains
< X-Content-Type-Options: nosniff
< X-DNS-Prefetch-Control: off
< X-Download-Options: noopen
< X-Frame-Options: SAMEORIGIN
< X-Permitted-Cross-Domain-Policies: none
< X-XSS-Protection: 0
< Access-Control-Allow-Origin: http://192.168.178.134:8081
< Vary: Origin
< Access-Control-Allow-Credentials: true
< Access-Control-Expose-Headers: Content-Type,Authorization
< content-type: application/json
< Date: Sat, 09 Aug 2025 04:50:34 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
< 
* Connection #0 to host 192.168.178.134 left intact
  • Issue persists with/without Redis secondary storage
  • Issue persists with/without custom session plugins
  • Workaround: Direct database queries work fine, suggesting the issue is in BetterAuth's internal session validation logic
  • Similar to reported issue #2055 but occurs even with correct CORS configuration
  • Affects both auth.api.getSession() programmatic calls and direct HTTP requests to /get-session
Originally created by @SushiiReboot on GitHub (Aug 9, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce: 1. Set up BetterAuth with Prisma adapter and MySQL database 2. Configure Express server with BetterAuth routes at /api/auth/* 3. Create a user account and sign in successfully (session created in database) 4. Call auth.api.getSession() with valid headers containing session cookie 5. Observe that API returns null despite valid session existing in database ### Reproduction steps: Note: Replace [SESSION] with the token that was generated upon signup. ### 1. Session exists in database ``` docker exec freskapp-backend-db-1 mysql -u root -p freskapp_db -e "SELECT token, userId, expiresAt FROM session WHERE token = [SESSION];" # Returns: valid session with future expiry date ``` ### 2. HTTP endpoint works and returns 200 OK but null body ``` curl -X GET "http://192.168.178.134:3000/api/auth/get-session" \ -H "Cookie: freskapp.session_token=[SESSION]" \ -H "Origin: http://192.168.178.134:8081" -v ``` Returns: 200 OK with CORS headers but body is null ### 3. auth.api.getSession() in middleware also returns null ``` const session = await auth.api.getSession({ headers: fromNodeHeaders(req.headers) }); ``` Returns: null ### Current vs. Expected behavior ### Current: - auth.api.getSession() returns null - HTTP endpoint /api/auth/get-session returns 200 OK with null body - Session token exists in database and is valid (not expired) - CORS headers are correctly set (Access-Control-Allow-Origin matches request origin) ### Expected: - auth.api.getSession() should return { user: {...}, session: {...} } - HTTP endpoint should return the user and session data from database - Valid sessions in database should be recognized and returned ### What version of Better Auth are you using? 1.3.4 ### Provide environment information ```bash - OS: macOS - Node.js: Latest - Database: MySQL 8.0 (Docker) - Server: Express.js - Client: React Native Expo (Android emulator) - Network: Local development (HTTP, not HTTPS) ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript export const auth = betterAuth({ appName: "Freskapp", secret: process.env.BETTER_AUTH_SECRET, baseURL: "http://10.0.2.2:3000", database: prismaAdapter(prisma, { provider: "mysql" }), emailAndPassword: { enabled: true, autoSignIn: true, requireEmailVerification: false }, plugins: [expo()], advanced: { cookiePrefix: "freskapp", useSecureCookies: false, defaultCookieAttributes: { sameSite: "none", secure: false, httpOnly: true, }, }, trustedOrigins: [ "http://192.168.178.134:3000", "http://192.168.178.134:8081", "http://10.0.2.2:3000", "http://10.0.2.2:8081", "http://localhost:3000", "freskapp://" ], logger: { level: "debug", disabled: false } }); ``` ### Additional context - Session creation works perfectly - users can sign up/in successfully - Database contains valid session data with correct expiry dates - CORS is working correctly (verified with verbose cURL output) Here's are an example with the call and the response: ``` curl -X GET "http://192.168.178.134:3000/api/auth/get-session" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -H "Cookie: freskapp.session_token=vYRKSB8ZiorLLdbJUYe1WpR4P47cDTZS" \ -H "User-Agent: Mozilla/5.0 (Android 14; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0" \ -H "Origin: http://192.168.178.134:8081" \ -H "Referer: http://192.168.178.134:8081/" \ -H "X-Requested-With: XMLHttpRequest" \ -H "Cache-Control: no-cache" \ -v ``` ``` Note: Unnecessary use of -X or --request, GET is already inferred. * Trying 192.168.178.134:3000... * Connected to 192.168.178.134 (192.168.178.134) port 3000 > GET /api/auth/get-session HTTP/1.1 > Host: 192.168.178.134:3000 > Content-Type: application/json > Accept: application/json > Cookie: freskapp.session_token=vYRKSB8ZiorLLdbJUYe1WpR4P47cDTZS > User-Agent: Mozilla/5.0 (Android 14; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0 > Origin: http://192.168.178.134:8081 > Referer: http://192.168.178.134:8081/ > X-Requested-With: XMLHttpRequest > Cache-Control: no-cache > * Request completely sent off < HTTP/1.1 200 OK < Cross-Origin-Resource-Policy: same-origin < Referrer-Policy: no-referrer < Strict-Transport-Security: max-age=31536000; includeSubDomains < X-Content-Type-Options: nosniff < X-DNS-Prefetch-Control: off < X-Download-Options: noopen < X-Frame-Options: SAMEORIGIN < X-Permitted-Cross-Domain-Policies: none < X-XSS-Protection: 0 < Access-Control-Allow-Origin: http://192.168.178.134:8081 < Vary: Origin < Access-Control-Allow-Credentials: true < Access-Control-Expose-Headers: Content-Type,Authorization < content-type: application/json < Date: Sat, 09 Aug 2025 04:50:34 GMT < Connection: keep-alive < Keep-Alive: timeout=5 < Transfer-Encoding: chunked < * Connection #0 to host 192.168.178.134 left intact ``` - Issue persists with/without Redis secondary storage - Issue persists with/without custom session plugins - Workaround: Direct database queries work fine, suggesting the issue is in BetterAuth's internal session validation logic - Similar to reported issue #2055 but occurs even with correct CORS configuration - Affects both auth.api.getSession() programmatic calls and direct HTTP requests to /get-session
GiteaMirror added the expressbug labels 2026-03-13 08:55:02 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 9, 2025):

Hi @SushiiReboot! I'm Dosu and I’m helping the better-auth team.

This is suited for GitHub—there are multiple confirmed bugs in BetterAuth v1.3.4 that can cause auth.api.getSession() and /api/auth/get-session to return null even when the session exists in the database. The most relevant are:

  • If you use both customSession and cookieCache, getSession may return null or stale data because the session_token cookie isn't refreshed. Workarounds: use the disableCookieCache query param on getSession, disable cookieCache, or upgrade to a newer BetterAuth version with the fix (details).
  • A bug (fixed after v1.3.4) caused getSession to return null and clear the session cookie due to a mismatch in expiresAt timestamps between the cookie and its signature (details).
  • Another bug (fixed after v1.3.4) caused getSession to early-return without adding the session to the context when session data was present in the cookie, so hooks and auth.api.getSession() would get null (details).
  • If you use @better-auth/expo, make sure you're on at least v1.2.9—older versions have cookie expiration bugs that cause valid cookies to be treated as expired (details).

Also, double-check that your cookiePrefix, secure flag, and trustedOrigins match between client and server, and that cookies are actually reaching the backend (log raw request headers to confirm). Your config looks correct for local dev, but any mismatch in cookie name or attributes will cause getSession to return null (details).

Upgrading BetterAuth and @better-auth/expo to the latest versions should resolve these issues. If you can't upgrade, try the workarounds above.

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 9, 2025): <!-- Greeting --> Hi @SushiiReboot! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is suited for GitHub—there are multiple confirmed bugs in BetterAuth v1.3.4 that can cause auth.api.getSession() and /api/auth/get-session to return null even when the session exists in the database. The most relevant are: - If you use both customSession and cookieCache, getSession may return null or stale data because the session_token cookie isn't refreshed. Workarounds: use the disableCookieCache query param on getSession, disable cookieCache, or upgrade to a newer BetterAuth version with the fix ([details](https://github.com/better-auth/better-auth/issues/3713)). - A bug (fixed after v1.3.4) caused getSession to return null and clear the session cookie due to a mismatch in expiresAt timestamps between the cookie and its signature ([details](https://github.com/better-auth/better-auth/pull/3283)). - Another bug (fixed after v1.3.4) caused getSession to early-return without adding the session to the context when session data was present in the cookie, so hooks and auth.api.getSession() would get null ([details](https://github.com/better-auth/better-auth/pull/3578)). - If you use @better-auth/expo, make sure you're on at least v1.2.9—older versions have cookie expiration bugs that cause valid cookies to be treated as expired ([details](https://github.com/better-auth/better-auth/issues/3180)). Also, double-check that your cookiePrefix, secure flag, and trustedOrigins match between client and server, and that cookies are actually reaching the backend (log raw request headers to confirm). Your config looks correct for local dev, but any mismatch in cookie name or attributes will cause getSession to return null ([details](https://github.com/better-auth/better-auth/issues/2196)). Upgrading BetterAuth and @better-auth/expo to the latest versions should resolve these issues. If you can't upgrade, try the workarounds above. <!-- 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/7dc4a077-d2a6-4ecf-836b-e67d1de84497?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/7dc4a077-d2a6-4ecf-836b-e67d1de84497?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/7dc4a077-d2a6-4ecf-836b-e67d1de84497?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/7dc4a077-d2a6-4ecf-836b-e67d1de84497?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/7dc4a077-d2a6-4ecf-836b-e67d1de84497?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/7dc4a077-d2a6-4ecf-836b-e67d1de84497?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/7dc4a077-d2a6-4ecf-836b-e67d1de84497?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/3892)
Author
Owner

@SushiiReboot commented on GitHub (Aug 9, 2025):

@dosu

Thank you for the detailed analysis! I've already implemented several of your suggested fixes:

  • Upgraded to v1.3.5-beta.5: Both better-auth and @better-auth/expo are on the latest beta version

  • Using disableCookieCache: I'm calling
    getSession({ query: { disableCookieCache: true } })

  • Cookie configuration verified: cookiePrefix, secure flags, and trustedOrigins all match between client/server -

  • Raw headers confirmed: Cookies are reaching the backend
    correctly with proper format freskapp.session_token=...

  • Database validation: Session tokens exist in database and are not expired

    However, the issue persists in v1.3.5-beta.5:

    • auth.api.getSession() returns null
    • /api/auth/get-session endpoint returns 200 OK but with null body
    • Session exists in database with valid expiration date
    • No errors thrown, API calls are "successful"

    My debugging shows (with a token from a debug session):

  // Headers being passed to BetterAuth are correct
  Headers: { cookie: 'freskapp.session_token=OyRcYF4gswrsW8eHPudShJrJfq6xCXY6', ... }
  // Database contains valid session
  mysql> SELECT token, expiresAt FROM session WHERE token = 'OyRcYF4gswrsW8eHPudShJrJfq6xCXY6';
  token: OyRcYF4gswrsW8eHPudShJrJfq6xCXY6 | expiresAt: 2025-08-16 05:51:53.284
  // But BetterAuth returns null
  Session result: null

Since i'm on the latest beta version with all the fixes you mentioned, this appears to be a new or remaining bug in the session validation logic. The issue seems to be that BetterAuth is successfully retrieving session data internally but then rejecting it during validation for unknown reasons.

Here are the logs of the local dev server if someone needs them:

 Connected to Redis
  ✅ Prisma connected successfully
  📊 User count in DB: 47
  📊 Session count in DB: 4
  === AUTH DEBUG ===
  Raw Headers: {
    "content-type": "application/json",
    "accept": "application/json",
    "cache-control": "no-cache",
    "cookie": "freskapp.session_token=OyRcYF4gswrsW8eHPudShJrJfq6xCXY6",
    "host": "192.168.178.134:3000",
    "connection": "Keep-Alive",
    "accept-encoding": "gzip",
    "user-agent": "okhttp/4.12.0"
  }
  Raw Cookie Header: freskapp.session_token=OyRcYF4gswrsW8eHPudShJrJfq6xCXY6
  Cookie Parsed: [ 'freskapp.session_token=OyRcYF4gswrsW8eHPudShJrJfq6xCXY6' ]
  Session Token: OyRcYF4gswrsW8eHPudShJrJfq6xCXY6
  Host: 192.168.178.134:3000
  Origin: undefined
  Referer: undefined
  User-Agent: okhttp/4.12.0
  Calling auth.api.getSession...
  Headers being passed to BetterAuth: {
    accept: 'application/json',
    'accept-encoding': 'gzip',
    'cache-control': 'no-cache',
    connection: 'Keep-Alive',
    'content-type': 'application/json',
    cookie: 'freskapp.session_token=OyRcYF4gswrsW8eHPudShJrJfq6xCXY6',
    host: '192.168.178.134:3000',
    'user-agent': 'okhttp/4.12.0'
  }
  ✅ Session API call successful
  Session result: null
  Session type: object
  Session keys: no keys
  Session user: undefined
  Session session: undefined
  Session user type: undefined
  Session session type: undefined
  Trying without cookie cache...
  Session without cache: null

Would you recommend any additional debugging steps or further workarounds for this scenario?

@SushiiReboot commented on GitHub (Aug 9, 2025): @dosu Thank you for the detailed analysis! I've already implemented several of your suggested fixes: - Upgraded to v1.3.5-beta.5: Both better-auth and @better-auth/expo are on the latest beta version - Using disableCookieCache: I'm calling ` getSession({ query: { disableCookieCache: true } })` - Cookie configuration verified: cookiePrefix, secure flags, and trustedOrigins all match between client/server - - Raw headers confirmed: Cookies are reaching the backend correctly with proper format freskapp.session_token=... - Database validation: Session tokens exist in database and are not expired However, the issue persists in v1.3.5-beta.5: - auth.api.getSession() returns null - /api/auth/get-session endpoint returns 200 OK but with null body - Session exists in database with valid expiration date - No errors thrown, API calls are "successful" My debugging shows (with a token from a debug session): ``` // Headers being passed to BetterAuth are correct Headers: { cookie: 'freskapp.session_token=OyRcYF4gswrsW8eHPudShJrJfq6xCXY6', ... } ``` ``` // Database contains valid session mysql> SELECT token, expiresAt FROM session WHERE token = 'OyRcYF4gswrsW8eHPudShJrJfq6xCXY6'; token: OyRcYF4gswrsW8eHPudShJrJfq6xCXY6 | expiresAt: 2025-08-16 05:51:53.284 ``` ``` // But BetterAuth returns null Session result: null ``` Since i'm on the latest beta version with all the fixes you mentioned, this appears to be a new or remaining bug in the session validation logic. The issue seems to be that BetterAuth is successfully retrieving session data internally but then rejecting it during validation for unknown reasons. Here are the logs of the local dev server if someone needs them: ``` Connected to Redis ✅ Prisma connected successfully 📊 User count in DB: 47 📊 Session count in DB: 4 === AUTH DEBUG === Raw Headers: { "content-type": "application/json", "accept": "application/json", "cache-control": "no-cache", "cookie": "freskapp.session_token=OyRcYF4gswrsW8eHPudShJrJfq6xCXY6", "host": "192.168.178.134:3000", "connection": "Keep-Alive", "accept-encoding": "gzip", "user-agent": "okhttp/4.12.0" } Raw Cookie Header: freskapp.session_token=OyRcYF4gswrsW8eHPudShJrJfq6xCXY6 Cookie Parsed: [ 'freskapp.session_token=OyRcYF4gswrsW8eHPudShJrJfq6xCXY6' ] Session Token: OyRcYF4gswrsW8eHPudShJrJfq6xCXY6 Host: 192.168.178.134:3000 Origin: undefined Referer: undefined User-Agent: okhttp/4.12.0 Calling auth.api.getSession... Headers being passed to BetterAuth: { accept: 'application/json', 'accept-encoding': 'gzip', 'cache-control': 'no-cache', connection: 'Keep-Alive', 'content-type': 'application/json', cookie: 'freskapp.session_token=OyRcYF4gswrsW8eHPudShJrJfq6xCXY6', host: '192.168.178.134:3000', 'user-agent': 'okhttp/4.12.0' } ✅ Session API call successful Session result: null Session type: object Session keys: no keys Session user: undefined Session session: undefined Session user type: undefined Session session type: undefined Trying without cookie cache... Session without cache: null ``` Would you recommend any additional debugging steps or further workarounds for this scenario?
Author
Owner

@dosubot[bot] commented on GitHub (Aug 9, 2025):

This looks like a silent validation failure in BetterAuth’s session logic—your config and database are correct, but the session is being rejected internally without errors. The most likely causes are schema mismatches or missing fields in your user/session records, or a bug in the session context handling.

Here’s what to check and try next:

  1. Schema conformity: Manually inspect your session and user tables. Make sure every session has a valid userId, expiresAt, createdAt, updatedAt, and token (all non-null, correct types). For users, check for id, email (lowercase), emailVerified, name, createdAt, and updatedAt. Any missing or mis-typed field will cause silent rejection (schema details).

  2. Minimal config test: Temporarily remove all plugins and custom fields from your BetterAuth config. Create a new user/session and test getSession. If it works, reintroduce plugins one by one to isolate the cause.

  3. Client diversity: Try retrieving the session from a browser, curl, and your mobile client. If one works and others don’t, it’s likely a cookie or header issue.

  4. Recent fixes: There was a bug in session context handling fixed in PR #3578 (merged 2025-07-24). If your beta predates this, you may not have the fix—upgrading to a build after this PR could resolve the issue.

  5. Debugging limitations: Even with debug logging enabled, BetterAuth does not output detailed validation traces for session rejection (logging details). For deeper introspection, you can add hooks to your database adapter or wrap the logger to print more info during session/user parsing.

If none of these steps reveal the issue, prepare a minimal reproducible example with your schema and config—this will help maintainers pinpoint the bug. Let me know what you find!

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 9, 2025): <!-- Answer --> This looks like a silent validation failure in BetterAuth’s session logic—your config and database are correct, but the session is being rejected internally without errors. The most likely causes are schema mismatches or missing fields in your user/session records, or a bug in the session context handling. Here’s what to check and try next: 1. **Schema conformity:** Manually inspect your `session` and `user` tables. Make sure every session has a valid `userId`, `expiresAt`, `createdAt`, `updatedAt`, and `token` (all non-null, correct types). For users, check for `id`, `email` (lowercase), `emailVerified`, `name`, `createdAt`, and `updatedAt`. Any missing or mis-typed field will cause silent rejection ([schema details](https://github.com/better-auth/better-auth/blob/0fe17704048bca8d52b547eb57a53ca89e84b78c/packages/better-auth/src/db/schema.ts)). 2. **Minimal config test:** Temporarily remove all plugins and custom fields from your BetterAuth config. Create a new user/session and test getSession. If it works, reintroduce plugins one by one to isolate the cause. 3. **Client diversity:** Try retrieving the session from a browser, curl, and your mobile client. If one works and others don’t, it’s likely a cookie or header issue. 4. **Recent fixes:** There was a bug in session context handling fixed in [PR #3578](https://github.com/better-auth/better-auth/pull/3578) (merged 2025-07-24). If your beta predates this, you may not have the fix—upgrading to a build after this PR could resolve the issue. 5. **Debugging limitations:** Even with debug logging enabled, BetterAuth does not output detailed validation traces for session rejection ([logging details](https://github.com/better-auth/better-auth/issues/3169)). For deeper introspection, you can add hooks to your database adapter or wrap the logger to print more info during session/user parsing. If none of these steps reveal the issue, prepare a minimal reproducible example with your schema and config—this will help maintainers pinpoint the bug. Let me know what you find! <!-- 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/9f14a14c-9f17-4bb6-bdf1-79c06bd55db8?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/9f14a14c-9f17-4bb6-bdf1-79c06bd55db8?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/9f14a14c-9f17-4bb6-bdf1-79c06bd55db8?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/9f14a14c-9f17-4bb6-bdf1-79c06bd55db8?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/9f14a14c-9f17-4bb6-bdf1-79c06bd55db8?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/9f14a14c-9f17-4bb6-bdf1-79c06bd55db8?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/9f14a14c-9f17-4bb6-bdf1-79c06bd55db8?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/3892)
Author
Owner

@SushiiReboot commented on GitHub (Aug 9, 2025):

@dosu

I've systematically worked through your suggestions and verified the schema conformity, all required fields are present and non-null:

  • Session: id, userId, expiresAt, createdAt, updatedAt, token (all varchar/datetime, no nulls)
  • User: id, email, emailVerified, name, createdAt, updatedAt (all present, correct types)

Also I tripped down to bare minimum:

  const options = {
      secret: env.BETTER_AUTH_SECRET,
      baseURL: "http://10.0.2.2:3000",
      database: prismaAdapter(prisma, { provider: "mysql" }),
      emailAndPassword: { enabled: true }
  };

Result: Issue persists even with minimal config.

Added Prisma query logging and custom logger wrapper. The logs show BetterAuth is successfully querying sessions by token, finding the correct session data and making repeated successful database calls. But /api/auth/get-session still returns null.

Tested with cURL, mobile client, and direct HTTP calls - all return null consistently, so it's not a client-specific issue.

This appears to be a new regression or an unfixed edge case in the session validation pipeline. The bug manifests even with the simplest possible configuration, suggesting it's in BetterAuth's core session handling logic.

Minimal Reproduction

Environment

  • BetterAuth: 1.3.5-beta.5
  • @better-auth/expo: 1.3.5-beta.5
  • Database: MySQL 8.0
  • ORM: Prisma
  • Server: Express.js v4.21.2
  • Node.js: v20.19.3

Minimal BetterAuth Config

// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "../../generated/prisma";

const prisma = new PrismaClient();

export const auth = betterAuth({
    secret: process.env.BETTER_AUTH_SECRET,
    baseURL: "http://10.0.2.2:3000",
    database: prismaAdapter(prisma, {
        provider: "mysql"
    }),
    emailAndPassword: {
        enabled: true
    }
});

Minimal Express Setup

// src/app.ts
import express from "express";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./lib/auth";

export function createApp() {
    const app = express();

    // Basic CORS
    app.use((req, res, next) => {
        res.header('Access-Control-Allow-Origin', '*');
        res.header('Access-Control-Allow-Credentials', 'true');
        res.header('Access-Control-Allow-Headers', 'Origin,Content-Type,Accept,Cookie');
        next();
    });

    app.all("/api/auth/*", toNodeHandler(auth));
    app.use(express.json());

    return app;
}

Database Schema (Prisma)

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id            String    @id @default(cuid())
  email         String    @unique
  name          String?
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  emailVerified Boolean
  image         String?
  sessions      Session[]
  accounts      Account[]

  @@map("user")
}

model Session {
  id        String   @id
  expiresAt DateTime
  token     String   @unique
  createdAt DateTime
  updatedAt DateTime
  ipAddress String?
  userAgent String?
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("session")
}

model Account {
  id                    String    @id
  accountId             String
  providerId            String
  userId                String
  user                  User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  accessToken           String?
  refreshToken          String?
  idToken               String?
  accessTokenExpiresAt  DateTime?
  refreshTokenExpiresAt DateTime?
  scope                 String?
  password              String?
  createdAt             DateTime
  updatedAt             DateTime

  @@map("account")
}

Reproduction Steps

  1. Setup: Create user account via /api/auth/sign-up/email
  2. Verify: Confirm session exists in database:
    SELECT token, userId, expiresAt FROM session ORDER BY createdAt DESC LIMIT 1; (returns: token exists, valid expiry date)
  3. Test: Call get-session endpoint:
  curl "http://localhost:3000/api/auth/get-session" \
    -H "Cookie: freskapp.session_token=<TOKEN_FROM_DB>"
  1. Expected: Session object with user dataActual: null (with 200 OK status)
  2. Debug: Add Prisma query logging:
const prisma = new PrismaClient({ log: ['query'] });
prisma.$on('query', (e) => {
    if (e.query.includes('session')) {
        console.log('Query:', e.query, 'Params:', e.params);
    }
});

Debug Output from my session:

  🔍 PRISMA Query: SELECT * FROM session WHERE token = ? LIMIT ? OFFSET ?
  🔍 PRISMA Params: ["JQa6G4xepnt6pVoUXyhADNzsbOdl0tIg",1,0]
  🔍 PRISMA Duration: 1ms

  🔍 PRISMA Query: SELECT * FROM user WHERE id = ? LIMIT ? OFFSET ?
  🔍 PRISMA Params: ["kXzxGS494NXLRKm90hGALavyEmOOHz8f",1,0]
  🔍 PRISMA Duration: 0ms

Database queries succeed and return valid data, but getSession() returns null.

@SushiiReboot commented on GitHub (Aug 9, 2025): @dosu I've systematically worked through your suggestions and verified the schema conformity, all required fields are present and non-null: - Session: id, userId, expiresAt, createdAt, updatedAt, token (all varchar/datetime, no nulls) - User: id, email, emailVerified, name, createdAt, updatedAt (all present, correct types) Also I tripped down to bare minimum: ``` const options = { secret: env.BETTER_AUTH_SECRET, baseURL: "http://10.0.2.2:3000", database: prismaAdapter(prisma, { provider: "mysql" }), emailAndPassword: { enabled: true } }; ``` Result: Issue persists even with minimal config. Added Prisma query logging and custom logger wrapper. The logs show BetterAuth is successfully querying sessions by token, finding the correct session data and making repeated successful database calls. But /api/auth/get-session still returns null. Tested with cURL, mobile client, and direct HTTP calls - all return null consistently, so it's not a client-specific issue. This appears to be a new regression or an unfixed edge case in the session validation pipeline. The bug manifests even with the simplest possible configuration, suggesting it's in BetterAuth's core session handling logic. ## Minimal Reproduction ### Environment - BetterAuth: 1.3.5-beta.5 - @better-auth/expo: 1.3.5-beta.5 - Database: MySQL 8.0 - ORM: Prisma - Server: Express.js v4.21.2 - Node.js: v20.19.3 ### Minimal BetterAuth Config ``` // src/lib/auth.ts import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { PrismaClient } from "../../generated/prisma"; const prisma = new PrismaClient(); export const auth = betterAuth({ secret: process.env.BETTER_AUTH_SECRET, baseURL: "http://10.0.2.2:3000", database: prismaAdapter(prisma, { provider: "mysql" }), emailAndPassword: { enabled: true } }); ``` ### Minimal Express Setup ``` // src/app.ts import express from "express"; import { toNodeHandler } from "better-auth/node"; import { auth } from "./lib/auth"; export function createApp() { const app = express(); // Basic CORS app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Headers', 'Origin,Content-Type,Accept,Cookie'); next(); }); app.all("/api/auth/*", toNodeHandler(auth)); app.use(express.json()); return app; } ``` ### Database Schema (Prisma) ``` // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String @unique name String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt emailVerified Boolean image String? sessions Session[] accounts Account[] @@map("user") } model Session { id String @id expiresAt DateTime token String @unique createdAt DateTime updatedAt DateTime ipAddress String? userAgent String? userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("session") } model Account { id String @id accountId String providerId String userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) accessToken String? refreshToken String? idToken String? accessTokenExpiresAt DateTime? refreshTokenExpiresAt DateTime? scope String? password String? createdAt DateTime updatedAt DateTime @@map("account") } ``` ### Reproduction Steps 1. Setup: Create user account via /api/auth/sign-up/email 2. Verify: Confirm session exists in database: ` SELECT token, userId, expiresAt FROM session ORDER BY createdAt DESC LIMIT 1;` (returns: token exists, valid expiry date) 3. Test: Call get-session endpoint: ``` curl "http://localhost:3000/api/auth/get-session" \ -H "Cookie: freskapp.session_token=<TOKEN_FROM_DB>" ``` 3. Expected: Session object with user dataActual: null (with 200 OK status) 4. Debug: Add Prisma query logging: ``` const prisma = new PrismaClient({ log: ['query'] }); prisma.$on('query', (e) => { if (e.query.includes('session')) { console.log('Query:', e.query, 'Params:', e.params); } }); ``` Debug Output from my session: ``` 🔍 PRISMA Query: SELECT * FROM session WHERE token = ? LIMIT ? OFFSET ? 🔍 PRISMA Params: ["JQa6G4xepnt6pVoUXyhADNzsbOdl0tIg",1,0] 🔍 PRISMA Duration: 1ms 🔍 PRISMA Query: SELECT * FROM user WHERE id = ? LIMIT ? OFFSET ? 🔍 PRISMA Params: ["kXzxGS494NXLRKm90hGALavyEmOOHz8f",1,0] 🔍 PRISMA Duration: 0ms ``` Database queries succeed and return valid data, but `getSession()` returns null.
Author
Owner

@dosubot[bot] commented on GitHub (Aug 9, 2025):

You've hit a subtle but critical bug: Prisma (with MySQL) returns date fields as strings, but BetterAuth expects JS Date objects for fields like expiresAt, createdAt, and updatedAt. This causes the session expiry check to always fail, so getSession() returns null—even when the session is valid in the DB. There’s no runtime type coercion in the current BetterAuth code for DB results, only for secondary storage like Redis, so this bug fits your symptoms exactly (see code reference).

Workaround:
Patch your Prisma adapter or add a middleware/hook to coerce these fields to Date objects after fetching from the DB, e.g.:

function coerceSessionDates(session) {
  if (!session) return session;
  return {
    ...session,
    expiresAt: new Date(session.expiresAt),
    createdAt: new Date(session.createdAt),
    updatedAt: new Date(session.updatedAt),
  };
}

Then, apply this to session and user objects before returning them to BetterAuth’s internals.

If you want to patch BetterAuth itself, add this coercion in the findSession logic after fetching from the DB, mirroring what’s already done for Redis/secondary storage.

This is a core bug in the current beta and not a config or schema issue. If you try this workaround and getSession() starts returning valid sessions, you’ve confirmed the root cause. This should be fixed in BetterAuth itself to avoid silent nulls for all Prisma/MySQL users.

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 9, 2025): <!-- Answer --> You've hit a subtle but critical bug: Prisma (with MySQL) returns date fields as strings, but BetterAuth expects JS Date objects for fields like `expiresAt`, `createdAt`, and `updatedAt`. This causes the session expiry check to always fail, so getSession() returns null—even when the session is valid in the DB. There’s no runtime type coercion in the current BetterAuth code for DB results, only for secondary storage like Redis, so this bug fits your symptoms exactly ([see code reference](https://github.com/better-auth/better-auth/blob/0fe17704048bca8d52b547eb57a53ca89e84b78c/packages/better-auth/src/db/internal-adapter.ts)). **Workaround:** Patch your Prisma adapter or add a middleware/hook to coerce these fields to `Date` objects after fetching from the DB, e.g.: ```js function coerceSessionDates(session) { if (!session) return session; return { ...session, expiresAt: new Date(session.expiresAt), createdAt: new Date(session.createdAt), updatedAt: new Date(session.updatedAt), }; } ``` Then, apply this to session and user objects before returning them to BetterAuth’s internals. If you want to patch BetterAuth itself, add this coercion in the `findSession` logic after fetching from the DB, mirroring what’s already done for Redis/secondary storage. This is a core bug in the current beta and not a config or schema issue. If you try this workaround and getSession() starts returning valid sessions, you’ve confirmed the root cause. This should be fixed in BetterAuth itself to avoid silent nulls for all Prisma/MySQL users. <!-- 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/982a4656-5030-475e-852a-47494b728aec?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/982a4656-5030-475e-852a-47494b728aec?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/982a4656-5030-475e-852a-47494b728aec?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/982a4656-5030-475e-852a-47494b728aec?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/982a4656-5030-475e-852a-47494b728aec?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/982a4656-5030-475e-852a-47494b728aec?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/982a4656-5030-475e-852a-47494b728aec?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/3892)
Author
Owner

@SushiiReboot commented on GitHub (Aug 9, 2025):

@dosu
I tried this, but that doesn't work.

const prisma = new PrismaClient().$extends({
    result: {
        session: {
            expiresAt: {
                needs: { expiresAt: true },
                compute(session) {
                    console.log('Coercing expiresAt to Date:', session.expiresAt);
                    const date = new Date(session.expiresAt);
                    console.log('Coerced expiresAt:', date);
                    return date;
                }
            },
            createdAt: {
                needs: { createdAt: true },
                compute(session) {
                    console.log('Coercing createdAt to Date:', session.createdAt);
                    const date = new Date(session.createdAt);
                    console.log('Coerced createdAt:', date);
                    return date;
                }
            },
            updatedAt: {
                needs: { updatedAt: true },
                compute(session) {
                    console.log('Coercing updatedAt to Date:', session.updatedAt);
                    const date = new Date(session.updatedAt);
                    console.log('Coerced updatedAt:', date);
                    return date;
                }
            }
        },
        user: {
            createdAt: {
                needs: { createdAt: true },
                compute(user) {
                    console.log('Coercing user createdAt to Date:', user.createdAt);
                    const date = new Date(user.createdAt);
                    console.log('Coerced user createdAt:', date);
                    return date;
                }
            },
            updatedAt: {
                needs: { updatedAt: true },
                compute(user) {
                    console.log('Coercing user updatedAt to Date:', user.updatedAt);
                    const date = new Date(user.updatedAt);
                    console.log('Coerced user updatedAt:', date);
                    return date;
                }
            }
        }
    }
});

Here are the logs:

Coercing user createdAt to Date: 2025-08-09T07:25:18.545Z
Coerced user createdAt: 2025-08-09T07:25:18.545Z
Coercing user updatedAt to Date: 2025-08-09T07:25:18.545Z
Coerced user updatedAt: 2025-08-09T07:25:18.545Z
Coercing expiresAt to Date: 2025-08-16T07:25:18.559Z
Coerced expiresAt: 2025-08-16T07:25:18.559Z
Coercing createdAt to Date: 2025-08-09T07:25:18.559Z
Coerced createdAt: 2025-08-09T07:25:18.559Z
Coercing updatedAt to Date: 2025-08-09T07:25:18.559Z
Coerced updatedAt: 2025-08-09T07:25:18.559Z
Coercing expiresAt to Date: 2025-08-16T07:25:18.559Z
Coerced expiresAt: 2025-08-16T07:25:18.559Z
Coercing createdAt to Date: 2025-08-09T07:25:18.559Z
@SushiiReboot commented on GitHub (Aug 9, 2025): @dosu I tried this, but that doesn't work. ``` const prisma = new PrismaClient().$extends({ result: { session: { expiresAt: { needs: { expiresAt: true }, compute(session) { console.log('Coercing expiresAt to Date:', session.expiresAt); const date = new Date(session.expiresAt); console.log('Coerced expiresAt:', date); return date; } }, createdAt: { needs: { createdAt: true }, compute(session) { console.log('Coercing createdAt to Date:', session.createdAt); const date = new Date(session.createdAt); console.log('Coerced createdAt:', date); return date; } }, updatedAt: { needs: { updatedAt: true }, compute(session) { console.log('Coercing updatedAt to Date:', session.updatedAt); const date = new Date(session.updatedAt); console.log('Coerced updatedAt:', date); return date; } } }, user: { createdAt: { needs: { createdAt: true }, compute(user) { console.log('Coercing user createdAt to Date:', user.createdAt); const date = new Date(user.createdAt); console.log('Coerced user createdAt:', date); return date; } }, updatedAt: { needs: { updatedAt: true }, compute(user) { console.log('Coercing user updatedAt to Date:', user.updatedAt); const date = new Date(user.updatedAt); console.log('Coerced user updatedAt:', date); return date; } } } } }); ``` Here are the logs: ``` Coercing user createdAt to Date: 2025-08-09T07:25:18.545Z Coerced user createdAt: 2025-08-09T07:25:18.545Z Coercing user updatedAt to Date: 2025-08-09T07:25:18.545Z Coerced user updatedAt: 2025-08-09T07:25:18.545Z Coercing expiresAt to Date: 2025-08-16T07:25:18.559Z Coerced expiresAt: 2025-08-16T07:25:18.559Z Coercing createdAt to Date: 2025-08-09T07:25:18.559Z Coerced createdAt: 2025-08-09T07:25:18.559Z Coercing updatedAt to Date: 2025-08-09T07:25:18.559Z Coerced updatedAt: 2025-08-09T07:25:18.559Z Coercing expiresAt to Date: 2025-08-16T07:25:18.559Z Coerced expiresAt: 2025-08-16T07:25:18.559Z Coercing createdAt to Date: 2025-08-09T07:25:18.559Z ```
Author
Owner

@dosubot[bot] commented on GitHub (Aug 9, 2025):

Prisma $extends hooks don’t work here because BetterAuth’s adapter reconstructs DB results internally and bypasses those transformations. To fix the date issue, set supportsDates: false in your Prisma adapter config—this forces BetterAuth to coerce all date fields from strings to JS Date objects after fetching from MySQL, which is required for session validation to work (code reference).

Here’s how to do it:

export const auth = betterAuth({
  // ...other config
  database: prismaAdapter(prisma, {
    provider: "mysql",
    supportsDates: false
  }),
  // ...
});

After this change, log the type of session.expiresAt inside your endpoint or middleware—it should be a JS Date, not a string. This should resolve the silent nulls from getSession. If it doesn’t, let me know and I’ll help dig deeper!

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 9, 2025): <!-- Answer --> Prisma $extends hooks don’t work here because BetterAuth’s adapter reconstructs DB results internally and bypasses those transformations. To fix the date issue, set `supportsDates: false` in your Prisma adapter config—this forces BetterAuth to coerce all date fields from strings to JS Date objects after fetching from MySQL, which is required for session validation to work ([code reference](https://github.com/better-auth/better-auth/blob/0fe17704048bca8d52b547eb57a53ca89e84b78c/packages/better-auth/src/adapters/create-adapter/index.ts)). Here’s how to do it: ```ts export const auth = betterAuth({ // ...other config database: prismaAdapter(prisma, { provider: "mysql", supportsDates: false }), // ... }); ``` After this change, log the type of `session.expiresAt` inside your endpoint or middleware—it should be a JS Date, not a string. This should resolve the silent nulls from getSession. If it doesn’t, let me know and I’ll help dig deeper! <!-- 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/388a5a97-6220-4bb7-b176-52cdb9018695?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/388a5a97-6220-4bb7-b176-52cdb9018695?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/388a5a97-6220-4bb7-b176-52cdb9018695?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/388a5a97-6220-4bb7-b176-52cdb9018695?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/388a5a97-6220-4bb7-b176-52cdb9018695?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/388a5a97-6220-4bb7-b176-52cdb9018695?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/388a5a97-6220-4bb7-b176-52cdb9018695?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/3892)
Author
Owner

@SushiiReboot commented on GitHub (Aug 9, 2025):

@dosu
There's no supportsDates in Prisma adapter config.

@SushiiReboot commented on GitHub (Aug 9, 2025): @dosu There's no supportsDates in Prisma adapter config.
Author
Owner

@SushiiReboot commented on GitHub (Aug 9, 2025):

Solution: Issue was sending non-URL-encoded cookies to middleware

I solved this issue. It was not a bug with BetterAuth but the problem was that I was sending non-URL-encoded cookies to my authentication middleware, which caused session validation to fail.

The Problem

When trying to manually extract and send session cookies in React Native:

  // Wrong approach - sends raw token without proper encoding
  const sessionData = await authClient.$store.get('session');
  headers['Cookie'] = `freskapp.session_token=${sessionData.token}`;

This resulted in sending a non-URL-encoded cookie to the middleware:
Cookie: freskapp.session_token=OgyWoCtgvcOQId1Q1TwllqNSQgKU5UKh
But BetterAuth expects the properly signed and URL-encoded cookie format:
Cookie: freskapp.session_token=8av1dZWkUSeFNjR0EMo6OaFSp6d7SQSY.BiLgzCK5RHiQnBejjbzumhdryeMEoUzr30gqA%2FST7iA%3D

The middleware couldn't validate the raw token because it was missing the signature and proper URL encoding.

The Solution

Use BetterAuth's official getCookie() method:

  // Correct approach - returns properly signed and URL-encoded cookie
  const cookies = authClient.getCookie();
  if (cookies) {
    headers['Cookie'] = cookies;
  }

Key Takeaway

The issue wasn't just retrieving the cookie wrong, it was that manually extracting tokens bypassed BetterAuth's
signing and URL encoding process. Always use authClient.getCookie() if you have a project with React Native Expo to ensure cookies are properly formatted for your authentication middleware. Closing the issue as resolved and hope this explanation will help someone in the future :)

@SushiiReboot commented on GitHub (Aug 9, 2025): # Solution: Issue was sending non-URL-encoded cookies to middleware I solved this issue. It was not a bug with BetterAuth but the problem was that I was sending non-URL-encoded cookies to my authentication middleware, which caused session validation to fail. ### The Problem When trying to manually extract and send session cookies in React Native: ``` // Wrong approach - sends raw token without proper encoding const sessionData = await authClient.$store.get('session'); headers['Cookie'] = `freskapp.session_token=${sessionData.token}`; ``` This resulted in sending a non-URL-encoded cookie to the middleware: ` Cookie: freskapp.session_token=OgyWoCtgvcOQId1Q1TwllqNSQgKU5UKh ` But BetterAuth expects the properly signed and URL-encoded cookie format: ` Cookie: freskapp.session_token=8av1dZWkUSeFNjR0EMo6OaFSp6d7SQSY.BiLgzCK5RHiQnBejjbzumhdryeMEoUzr30gqA%2FST7iA%3D` The middleware couldn't validate the raw token because it was missing the signature and proper URL encoding. ### The Solution Use BetterAuth's official getCookie() method: ``` // Correct approach - returns properly signed and URL-encoded cookie const cookies = authClient.getCookie(); if (cookies) { headers['Cookie'] = cookies; } ``` ### Key Takeaway The issue wasn't just retrieving the cookie wrong, it was that manually extracting tokens bypassed BetterAuth's signing and URL encoding process. Always use authClient.getCookie() if you have a project with React Native Expo to ensure cookies are properly formatted for your authentication middleware. Closing the issue as resolved and hope this explanation will help someone in the future :)
Author
Owner

@skempisty commented on GitHub (Aug 19, 2025):

@dosu

Manually extracting the cookie from SecureStore instead of having to use authClient.getCookie() would be much more convenient. Because I have to use authClient.getCookie(), I'm going to be forced to make my "request" function dependent on that authClient getting passed. Can whatever authClient.getCookie() does be replicated without actually using it?

Like the OP, I was trying to do

authToken = await SecureStore.getItemAsync('myapp_cookie');

which was getting me this

{"better-auth.session_token":{"value":"E4RPgLxCdrEuTRB2Vo9Unp3oZvYXafGv.x4011OnqZ%2F4BJSWdW9e38kFydEjX2ne%2FDo6W0LDt1Tc%3D","expires":"2025-09-18T18:25:04.104Z"}}
@skempisty commented on GitHub (Aug 19, 2025): @dosu Manually extracting the cookie from SecureStore instead of having to use authClient.getCookie() would be much more convenient. Because I have to use authClient.getCookie(), I'm going to be forced to make my "request" function dependent on that authClient getting passed. Can whatever authClient.getCookie() does be replicated without actually using it? Like the OP, I was trying to do ``` authToken = await SecureStore.getItemAsync('myapp_cookie'); ``` which was getting me this ``` {"better-auth.session_token":{"value":"E4RPgLxCdrEuTRB2Vo9Unp3oZvYXafGv.x4011OnqZ%2F4BJSWdW9e38kFydEjX2ne%2FDo6W0LDt1Tc%3D","expires":"2025-09-18T18:25:04.104Z"}} ```
Author
Owner

@dosubot[bot] commented on GitHub (Aug 19, 2025):

Yes, you can replicate what authClient.getCookie() does without passing the client instance, but you need to follow BetterAuth’s signing and encoding logic exactly. The session cookie must be in the format <token>.<signature>, where the signature is generated using HMAC SHA-256 (base64urlnopad) with your BetterAuth secret, and the signature must be URL-encoded. If the value you get from SecureStore is already in this format (as in your example), you can use it directly in your Cookie header for requests—just make sure it’s not expired and matches your server config.

If you ever need to generate the cookie from a raw token, you’ll need to use BetterAuth’s signing utilities (see cookie signing details and code reference). There’s no official standalone helper yet, but you can use the exported functions from the cookies module for advanced use cases.

Bottom line: if SecureStore gives you the full signed cookie value, you’re good to go. If not, you’ll need to sign and encode it yourself using the same logic as BetterAuth.

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 19, 2025): <!-- Answer --> Yes, you can replicate what authClient.getCookie() does without passing the client instance, but you need to follow BetterAuth’s signing and encoding logic exactly. The session cookie must be in the format `<token>.<signature>`, where the signature is generated using HMAC SHA-256 (base64urlnopad) with your BetterAuth secret, and the signature must be URL-encoded. If the value you get from SecureStore is already in this format (as in your example), you can use it directly in your Cookie header for requests—just make sure it’s not expired and matches your server config. If you ever need to generate the cookie from a raw token, you’ll need to use BetterAuth’s signing utilities (see [cookie signing details](https://better-auth.com/docs/concepts/cookies) and [code reference](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/cookies/index.ts)). There’s no official standalone helper yet, but you can use the exported functions from the cookies module for advanced use cases. Bottom line: if SecureStore gives you the full signed cookie value, you’re good to go. If not, you’ll need to sign and encode it yourself using the same logic as BetterAuth. <!-- 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/702bbc1f-43ac-4509-8ca1-d3de2cd58a39?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/702bbc1f-43ac-4509-8ca1-d3de2cd58a39?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/702bbc1f-43ac-4509-8ca1-d3de2cd58a39?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/702bbc1f-43ac-4509-8ca1-d3de2cd58a39?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/702bbc1f-43ac-4509-8ca1-d3de2cd58a39?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/702bbc1f-43ac-4509-8ca1-d3de2cd58a39?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/702bbc1f-43ac-4509-8ca1-d3de2cd58a39?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/3892)
Author
Owner

@skempisty commented on GitHub (Aug 19, 2025):

@dosu

I'm confused because my expo app's authClient doesn't have access to the BETTER_AUTH_SECRET - my backend does.

Would it be possible to make this conversion on the BE using a "before" hook?

I would love to be able to pass the cookie I mentioned earlier -

{"better-auth.session_token":{"value":"E4RPgLxCdrEuTRB2Vo9Unp3oZvYXafGv.x4011OnqZ%2F4BJSWdW9e38kFydEjX2ne%2FDo6W0LDt1Tc%3D","expires":"2025-09-18T18:25:04.104Z"}}

and have the backend convert it into what Betterauth is expecting if possible. As it stands, without this workaround, I'd have to make all my api functions dependent on passing authClient (which I want to avoid if possible).

If this isn't an option, can you help me think of any other way to accomplish this?

@skempisty commented on GitHub (Aug 19, 2025): @dosu I'm confused because my expo app's authClient doesn't have access to the BETTER_AUTH_SECRET - my backend does. Would it be possible to make this conversion on the BE using a "before" hook? I would love to be able to pass the cookie I mentioned earlier - ``` {"better-auth.session_token":{"value":"E4RPgLxCdrEuTRB2Vo9Unp3oZvYXafGv.x4011OnqZ%2F4BJSWdW9e38kFydEjX2ne%2FDo6W0LDt1Tc%3D","expires":"2025-09-18T18:25:04.104Z"}} ``` and have the backend convert it into what Betterauth is expecting if possible. As it stands, without this workaround, I'd have to make all my api functions dependent on passing authClient (which I want to avoid if possible). If this isn't an option, can you help me think of any other way to accomplish this?
Author
Owner

@SushiiReboot commented on GitHub (Aug 19, 2025):

@skempisty

Hi! I have a question: " I'm going to be forced to make my "request" function dependent on that authClient getting passed" what do you mean by "pass the authClient"? The authclient is available globally so you don't need to pass it in the function paramaters. Can you give me an example of what you want to avoid please?

@SushiiReboot commented on GitHub (Aug 19, 2025): @skempisty Hi! I have a question: " I'm going to be forced to make my "request" function dependent on that authClient getting passed" what do you mean by "pass the authClient"? The authclient is available globally so you don't need to pass it in the function paramaters. Can you give me an example of what you want to avoid please?
Author
Owner

@skempisty commented on GitHub (Aug 19, 2025):

@SushiiReboot

Wherever else I've used authClient I've had to import it like this
import { authClient } from "@/lib/auth-client";

Am I missing something? How do I make it available globally? I've actually extracted out my "makeRequest" function to a separate package in my monorepo so usually anything it needs, it needs to be passed.

When I try to do
const cookie = authClient.getCookie();

without importing it, it throws an error (ofc its not available)

@skempisty commented on GitHub (Aug 19, 2025): @SushiiReboot Wherever else I've used authClient I've had to import it like this `import { authClient } from "@/lib/auth-client";` Am I missing something? How do I make it available globally? I've actually extracted out my "makeRequest" function to a separate package in my monorepo so usually anything it needs, it needs to be passed. When I try to do `const cookie = authClient.getCookie();` without importing it, it throws an error (ofc its not available)
Author
Owner

@SushiiReboot commented on GitHub (Aug 19, 2025):

@skempisty

Oh yeah by "making it available globally" I meant just importing it. I thought you were passing it as a parameter my bad. So you have the "makeRequest" function into a separate package right? Why don't you make a Dependency Injection with a config setter like this (for example idk your code):

// shared/config.ts
let authClient: AuthClient | null = null;

export function setAuthClient(client: AuthClient) {
  authClient = client;
}

export function getAuthClient() {
  if (!authClient) throw new Error("authClient not set!");
  return authClient;
}

Then in your main app entry point:

import { authClient } from "@/lib/auth-client";
import { setAuthClient } from "@myorg/shared/config";

setAuthClient(authClient);

and to use the client:

import { getAuthClient } from "@myorg/shared/config";

const cookie = getAuthClient().getCookie();

Honestly it's better to use BetterAuth's getCookie() function instead to try to fight against it (trust me I lost countless hours to this). If your main concern is your code to be tight coupled with BetterAuth's system, use a Clean Architecture pattern.

@SushiiReboot commented on GitHub (Aug 19, 2025): @skempisty Oh yeah by "making it available globally" I meant just importing it. I thought you were passing it as a parameter my bad. So you have the "makeRequest" function into a separate package right? Why don't you make a Dependency Injection with a config setter like this (for example idk your code): ``` // shared/config.ts let authClient: AuthClient | null = null; export function setAuthClient(client: AuthClient) { authClient = client; } export function getAuthClient() { if (!authClient) throw new Error("authClient not set!"); return authClient; } ``` Then in your main app entry point: ``` import { authClient } from "@/lib/auth-client"; import { setAuthClient } from "@myorg/shared/config"; setAuthClient(authClient); ``` and to use the client: ``` import { getAuthClient } from "@myorg/shared/config"; const cookie = getAuthClient().getCookie(); ``` Honestly it's better to use BetterAuth's getCookie() function instead to try to fight against it (trust me I lost countless hours to this). If your main concern is your code to be tight coupled with BetterAuth's system, use a Clean Architecture pattern.
Author
Owner

@skempisty commented on GitHub (Aug 19, 2025):

@SushiiReboot

That seems like a solid suggestion - I'll give it a shot. Thanks for the help!

I agree trying to get around using the standard better-auth patterns is probably a bad idea

@skempisty commented on GitHub (Aug 19, 2025): @SushiiReboot That seems like a solid suggestion - I'll give it a shot. Thanks for the help! I agree trying to get around using the standard better-auth patterns is probably a bad idea
Author
Owner

@skempisty commented on GitHub (Aug 19, 2025):

@SushiiReboot

That did the trick! Saved me a ton of time - thanks for paying it forward!

@skempisty commented on GitHub (Aug 19, 2025): @SushiiReboot That did the trick! Saved me a ton of time - thanks for paying it forward!
Author
Owner

@SushiiReboot commented on GitHub (Aug 19, 2025):

@skempisty

No problem! You're welcome :)

@SushiiReboot commented on GitHub (Aug 19, 2025): @skempisty No problem! You're welcome :)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1672