[GH-ISSUE #2115] Session cookie maxAge is not refreshed #9057

Closed
opened 2026-04-13 04:20:34 -05:00 by GiteaMirror · 35 comments
Owner

Originally created by @ck-euan on GitHub (Apr 4, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/2115

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Have a short session maxAge and updateAge:
    session:{ expiriesIn:60, updateAge:10 }

  2. When the updateAge has passed, call getSession

Current vs. Expected behavior

Currently the session in the database is refreshed and extended but the session cookie is not.
In the above example, the cookie expires after 60 seconds and you will be logged out.

Expected behaviour is that whenever the session is updated the cookie is also updated with the new expiry.

In my app I have 30 minute maxAge and 5 minute updateAge. It's expected that as you use the app your session is constantly extended until you stop using at and the app stops calling getSession. However what actually happens is the user is always logged out after 30 minutes.

What version of Better Auth are you using?

1.2.5

Provide environment information

- OS: Mac OS 15.3.1 (24D70)
- Browser: AVersion 1.89.0 (60953)

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

Frontend

Originally created by @ck-euan on GitHub (Apr 4, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/2115 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Have a short session maxAge and updateAge: `session:{ expiriesIn:60, updateAge:10 }` 2. When the updateAge has passed, call getSession ### Current vs. Expected behavior Currently the session in the database is refreshed and extended but the session cookie is not. In the above example, the cookie expires after 60 seconds and you will be logged out. Expected behaviour is that whenever the session is updated the cookie is also updated with the new expiry. In my app I have 30 minute maxAge and 5 minute updateAge. It's expected that as you use the app your session is constantly extended until you stop using at and the app stops calling getSession. However what actually happens is the user is always logged out after 30 minutes. ### What version of Better Auth are you using? 1.2.5 ### Provide environment information ```bash - OS: Mac OS 15.3.1 (24D70) - Browser: AVersion 1.89.0 (60953) ``` ### Which area(s) are affected? (Select all that apply) Frontend
GiteaMirror added the lockedbug labels 2026-04-13 04:20:34 -05:00
Author
Owner

@dennisjnnh commented on GitHub (Apr 4, 2025):

are you using the customSession plugin? this could be a duplicate of #2106

<!-- gh-comment-id:2778026077 --> @dennisjnnh commented on GitHub (Apr 4, 2025): are you using the customSession plugin? this could be a duplicate of #2106
Author
Owner

@ck-euan commented on GitHub (Apr 4, 2025):

I am, but I've also tried it without any plugins and observed the same.
I removed everything I could from my authOptions, this is the most stripped back version I can get and it still happens:

const authOptions = {
	database: drizzleAdapter(db, {
		provider: "mysql",
		schema: {
			user: users,
			session: dashAuthSessions,
			account: dashAccounts,
			verification: dashVerifications,
		},
	}),

	session: {
		expiresIn: 60,
		updateAge: 10,
	},
	emailAndPassword: {
		enabled: true,
		password: {
			hash: hashPassword,
			verify: ({ hash, password }) => verifyPassword(hash, password),
		},
	},

	appName: "CK Dash",
	baseURL: env.NEXT_PUBLIC_BASE_URL,
	trustedOrigins: [
		`${env.NEXT_PUBLIC_BASE_URL}/login`,
		env.NEXT_PUBLIC_BASE_URL,
	],
} satisfies BetterAuthOptions;
<!-- gh-comment-id:2778075228 --> @ck-euan commented on GitHub (Apr 4, 2025): I am, but I've also tried it without any plugins and observed the same. I removed everything I could from my authOptions, this is the most stripped back version I can get and it still happens: ``` const authOptions = { database: drizzleAdapter(db, { provider: "mysql", schema: { user: users, session: dashAuthSessions, account: dashAccounts, verification: dashVerifications, }, }), session: { expiresIn: 60, updateAge: 10, }, emailAndPassword: { enabled: true, password: { hash: hashPassword, verify: ({ hash, password }) => verifyPassword(hash, password), }, }, appName: "CK Dash", baseURL: env.NEXT_PUBLIC_BASE_URL, trustedOrigins: [ `${env.NEXT_PUBLIC_BASE_URL}/login`, env.NEXT_PUBLIC_BASE_URL, ], } satisfies BetterAuthOptions; ```
Author
Owner

@dennisjnnh commented on GitHub (Apr 4, 2025):

alright, just asking because u explicitly name getSession, are you calling from the client? maybe try useSession. because in my case with stripped down auth config without customSession, the set-cookie header is clearly passed

<!-- gh-comment-id:2778084413 --> @dennisjnnh commented on GitHub (Apr 4, 2025): alright, just asking because u explicitly name getSession, are you calling from the client? maybe try useSession. because in my case with stripped down auth config without customSession, the set-cookie header is clearly passed
Author
Owner

@ck-euan commented on GitHub (Apr 4, 2025):

Sorry I should have been more specific, it's just any time the session is refreshed be it either useSession or api.getSession

<!-- gh-comment-id:2778099553 --> @ck-euan commented on GitHub (Apr 4, 2025): Sorry I should have been more specific, it's just any time the session is refreshed be it either useSession or api.getSession
Author
Owner

@Bekacru commented on GitHub (Apr 4, 2025):

If the cookie only isn't being refreshed, it's most likely due to the server not being able to set the cookie. This happens for example, if you're only calling auth.api.getSession on react server component or if you're not using nextCookies plugin –assuming you're using nextjs.

<!-- gh-comment-id:2779076591 --> @Bekacru commented on GitHub (Apr 4, 2025): If the cookie only isn't being refreshed, it's most likely due to the server not being able to set the cookie. This happens for example, if you're only calling `auth.api.getSession` on react server component or if you're not using nextCookies plugin –assuming you're using nextjs.
Author
Owner

@newkind commented on GitHub (Apr 4, 2025):

@Bekacru I'm actually fighting with the same issue as @ck-euan. I'm using Next.js and I do have nextCookies plugin added in my auth.ts - acutally that's the only plugin that I'm using currently.

So if my page.tsx looks like below, what would be the solution? I do have other client components on that page ie. User Button, but they're part of layout.tsx and not this particular page.

import { auth } from '@/lib/auth';
import { headers } from "next/headers"
import { redirect } from 'next/navigation'
import { getUserProjects } from '@/actions/getUserProjects';
import ProjectCard from '@/components/projects/ProjectsCard';

export default async function Projects() {
  const session = await auth.api.getSession({
    headers: await headers()
  });

  // If user is not logged in, redirect to sign in page
  if (!session) {
    redirect('/sign-in');
  }

  const { data: userProjects } = await getUserProjects(session.user.id);

  return (
    <>
      <h1 className="text-2xl font-semibold mb-6 text-gray-800">Projects</h1>

      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-6">
        {userProjects?.map((project, idx) => (
          <ProjectCard key={idx} project={project} idx={idx} />
        ))}
      </div>
    </>
  )
}
<!-- gh-comment-id:2779289231 --> @newkind commented on GitHub (Apr 4, 2025): @Bekacru I'm actually fighting with the same issue as @ck-euan. I'm using Next.js and I do have nextCookies plugin added in my `auth.ts` - acutally that's the only plugin that I'm using currently. So if my `page.tsx` looks like below, what would be the solution? I do have other client components on that page ie. User Button, but they're part of `layout.tsx` and not this particular page. ``` import { auth } from '@/lib/auth'; import { headers } from "next/headers" import { redirect } from 'next/navigation' import { getUserProjects } from '@/actions/getUserProjects'; import ProjectCard from '@/components/projects/ProjectsCard'; export default async function Projects() { const session = await auth.api.getSession({ headers: await headers() }); // If user is not logged in, redirect to sign in page if (!session) { redirect('/sign-in'); } const { data: userProjects } = await getUserProjects(session.user.id); return ( <> <h1 className="text-2xl font-semibold mb-6 text-gray-800">Projects</h1> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-6"> {userProjects?.map((project, idx) => ( <ProjectCard key={idx} project={project} idx={idx} /> ))} </div> </> ) } ```
Author
Owner

@dennisjnnh commented on GitHub (Apr 4, 2025):

@Bekacru I'm actually fighting with the same issue as @ck-euan. I'm using Next.js and I do have nextCookies plugin added in my auth.ts - acutally that's the only plugin that I'm using currently.

So if my page.tsx looks like below, what would be the solution? I do have other client components on that page ie. User Button, but they're part of layout.tsx and not this particular page.

import { auth } from '@/lib/auth';
import { headers } from "next/headers"
import { redirect } from 'next/navigation'
import { getUserProjects } from '@/actions/getUserProjects';
import ProjectCard from '@/components/projects/ProjectsCard';

export default async function Projects() {
  const session = await auth.api.getSession({
    headers: await headers()
  });

  // If user is not logged in, redirect to sign in page
  if (!session) {
    redirect('/sign-in');
  }

  const { data: userProjects } = await getUserProjects(session.user.id);

  return (
    <>
      <h1 className="text-2xl font-semibold mb-6 text-gray-800">Projects</h1>

      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-6">
        {userProjects?.map((project, idx) => (
          <ProjectCard key={idx} project={project} idx={idx} />
        ))}
      </div>
    </>
  )
}

can you test if fetching the session on a client component with useSession() resolves the issue?

<!-- gh-comment-id:2779400433 --> @dennisjnnh commented on GitHub (Apr 4, 2025): > [@Bekacru](https://github.com/Bekacru) I'm actually fighting with the same issue as [@ck-euan](https://github.com/ck-euan). I'm using Next.js and I do have nextCookies plugin added in my `auth.ts` - acutally that's the only plugin that I'm using currently. > > So if my `page.tsx` looks like below, what would be the solution? I do have other client components on that page ie. User Button, but they're part of `layout.tsx` and not this particular page. > > ``` > import { auth } from '@/lib/auth'; > import { headers } from "next/headers" > import { redirect } from 'next/navigation' > import { getUserProjects } from '@/actions/getUserProjects'; > import ProjectCard from '@/components/projects/ProjectsCard'; > > export default async function Projects() { > const session = await auth.api.getSession({ > headers: await headers() > }); > > // If user is not logged in, redirect to sign in page > if (!session) { > redirect('/sign-in'); > } > > const { data: userProjects } = await getUserProjects(session.user.id); > > return ( > <> > <h1 className="text-2xl font-semibold mb-6 text-gray-800">Projects</h1> > > <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-6"> > {userProjects?.map((project, idx) => ( > <ProjectCard key={idx} project={project} idx={idx} /> > ))} > </div> > </> > ) > } > ``` can you test if fetching the session on a client component with useSession() resolves the issue?
Author
Owner

@newkind commented on GitHub (Apr 4, 2025):

I've got this client component loading on all dashboard pages and it uses useSession() but even with this component present in the page I still have to do the manual full page refresh when the cookie expires.

"use client"

import { useRouter } from 'next/navigation'
import { authClient, useSession } from "@/lib/auth-client"

export default function UserButton() {
  const { data: session, isPending } = useSession();
  const user = session?.user;
  const router = useRouter();

  return (
    <>
      {(!isPending && user) && (

Also one more thing I noticed. When the cookie expires and the new one doesn't get generated, my server requests drastically slow down - from around 250ms to 1~3 seconds.

Image

Image

<!-- gh-comment-id:2779418358 --> @newkind commented on GitHub (Apr 4, 2025): I've got this client component loading on all dashboard pages and it uses useSession() but even with this component present in the page I still have to do the manual full page refresh when the cookie expires. ``` "use client" import { useRouter } from 'next/navigation' import { authClient, useSession } from "@/lib/auth-client" export default function UserButton() { const { data: session, isPending } = useSession(); const user = session?.user; const router = useRouter(); return ( <> {(!isPending && user) && ( ``` Also one more thing I noticed. When the cookie expires and the new one doesn't get generated, my server requests drastically slow down - from around 250ms to 1~3 seconds. ![Image](https://github.com/user-attachments/assets/7d0ebece-3e47-431b-9fac-31a83b151183) ![Image](https://github.com/user-attachments/assets/628242b8-928a-4023-a136-b89512ef2bf1)
Author
Owner

@Bekacru commented on GitHub (Apr 12, 2025):

@newkind the slow down is probably due to the cookie cache missing for those requests. But even then 1 to 3 seconds is high. Make sure to index your db fields specially token and userId field in the session table.

<!-- gh-comment-id:2799030492 --> @Bekacru commented on GitHub (Apr 12, 2025): @newkind the slow down is probably due to the cookie cache missing for those requests. But even then 1 to 3 seconds is high. Make sure to index your db fields specially `token` and `userId` field in the session table.
Author
Owner

@Kabeer10 commented on GitHub (Apr 17, 2025):

hey @Bekacru i also noticed a similar sure in latest version Discord. Im Just using thunder client for sign in and get-session

<!-- gh-comment-id:2811869703 --> @Kabeer10 commented on GitHub (Apr 17, 2025): hey @Bekacru i also noticed a similar sure in latest version [Discord](https://discord.com/channels/1288403910284935179/1362014986355867758). Im Just using thunder client for `sign in` and `get-session`
Author
Owner

@newkind commented on GitHub (Apr 17, 2025):

@Bekacru this is brand new database with only 1 user - me, so there's really not much to index there ;) Anyways, that was connection to my test instance of Supabase in US, while I'm in Europe. I switched to other test instance - this time based also in Europe so the times drastically went down, but the issue itself where there's a spike in request time whenever cookie expires is still relevant.

<!-- gh-comment-id:2812301454 --> @newkind commented on GitHub (Apr 17, 2025): @Bekacru this is brand new database with only 1 user - me, so there's really not much to index there ;) Anyways, that was connection to my test instance of Supabase in US, while I'm in Europe. I switched to other test instance - this time based also in Europe so the times drastically went down, but the issue itself where there's a spike in request time whenever cookie expires is still relevant.
Author
Owner

@Kabeer10 commented on GitHub (Apr 18, 2025):

hey @Bekacru Could it be because of this

  1. Lets Assume Session is due to update
    This code gets called in server component ,ie - Cookie cant be set, but session is updated in DB
 const session = await auth.api.getSession({
    headers: await headers()
  });
  1. This gets called in client component later (but updateAge is 10) so Technically session wont be updated here
 const { data: session, isPending } = useSession();

The solution ( Prevent Session update in Server Component )

 const session = await auth.api.getSession({
    headers: await headers(),
      query: {
          disableRefresh: true   // <--- Add this 
        }
  });
<!-- gh-comment-id:2814381509 --> @Kabeer10 commented on GitHub (Apr 18, 2025): hey @Bekacru Could it be because of this 1. Lets Assume Session is due to update This code gets called in server component ,ie - Cookie cant be set, but session is updated in DB ``` const session = await auth.api.getSession({ headers: await headers() }); ``` 2. This gets called in client component later (but updateAge is 10) so Technically session wont be updated here ``` const { data: session, isPending } = useSession(); ``` The solution ( Prevent Session update in Server Component ) ``` const session = await auth.api.getSession({ headers: await headers(), query: { disableRefresh: true // <--- Add this } }); ```
Author
Owner

@Bekacru commented on GitHub (May 5, 2025):

@Kabeer10 that's likely the case. We should probably have something built in for this so this doesn't happen.

<!-- gh-comment-id:2852098964 --> @Bekacru commented on GitHub (May 5, 2025): @Kabeer10 that's likely the case. We should probably have something built in for this so this doesn't happen.
Author
Owner

@s-petey commented on GitHub (May 23, 2025):

So for those that would prefer to use server side auth and apply refreshes it'd be nice to have this documented. I'm using logic so when someone requests my data, I'll validate their status, then if it is still valid re-set the cookie headers on that request / response. This way I can keep updating the auth status which comes from my middleware for any request(s) to the application.

For those that use NextJS there is a middleware, which I believe was mentioned above that should be applied.

However if it isn't working as middleware, you can manually apply this logic by using its methods. In my example I've just re-created this logic to be run any time I call auth.api.getSession I then get the raw response with the property asResponse which I can then pass directly to this logic.

Example of my method called after auth.apigetSession

// WARNING: This is mostly a copy of `next-js` in better-auth.
// import { nextCookies } from 'better-auth/next-js';
export function setServerCookies(headers: Headers, { cookies }: RequestEvent) {
  const setCookies = headers?.get('set-cookie');

  if (!setCookies) return;

  const parsed = parseSetCookieHeader(setCookies);

  for (const [key, value] of parsed) {
    if (!key) continue;

    const opts: Parameters<typeof cookies.set>[2] = {
      sameSite: value.samesite,
      secure: value.secure,
      maxAge: value['max-age'],
      httpOnly: value.httponly,
      domain: value.domain,
      path: value.path ?? '',
    };

    cookies.set(key, decodeURIComponent(value.value), opts);
  }
}

As a request to the author, it may be nice to offer returnHeaders on the getSession as there is on signUp{FORMAT) methods. Otherwise expose a method to call such as auth.api.refreshCookies. If not in the core package, expose it in the library specific packages. For me it'd be for SvelteKit, however the above examples were for NextJS.

EDIT:
The returnHeaders already exists on the internal implementation.

<!-- gh-comment-id:2905401448 --> @s-petey commented on GitHub (May 23, 2025): So for those that would prefer to use server side auth and apply refreshes it'd be nice to have this documented. I'm using logic so when someone requests my data, I'll validate their status, then if it is still valid re-set the cookie headers on that request / response. This way I can keep updating the auth status which comes from my middleware for any request(s) to the application. For those that use NextJS there is a [middleware](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/integrations/next-js.ts#L21), which I believe was mentioned above that should be applied. However if it isn't working as middleware, you can manually apply this logic by using its methods. In my example I've just re-created this logic to be run any time I call `auth.api.getSession` I then get the raw response with the property `asResponse` which I can then pass directly to [this logic](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/integrations/next-js.ts#L35-L56). Example of my method called after `auth.apigetSession` ```ts // WARNING: This is mostly a copy of `next-js` in better-auth. // import { nextCookies } from 'better-auth/next-js'; export function setServerCookies(headers: Headers, { cookies }: RequestEvent) { const setCookies = headers?.get('set-cookie'); if (!setCookies) return; const parsed = parseSetCookieHeader(setCookies); for (const [key, value] of parsed) { if (!key) continue; const opts: Parameters<typeof cookies.set>[2] = { sameSite: value.samesite, secure: value.secure, maxAge: value['max-age'], httpOnly: value.httponly, domain: value.domain, path: value.path ?? '', }; cookies.set(key, decodeURIComponent(value.value), opts); } } ``` As a request to the author, it may be nice to offer `returnHeaders` on the `getSession` as there is on `signUp{FORMAT)` methods. Otherwise expose a method to call such as `auth.api.refreshCookies`. If not in the core package, expose it in the library specific packages. For me it'd be for SvelteKit, however the above examples were for NextJS. EDIT: The `returnHeaders` already exists on the [internal implementation](https://github.com/better-auth/better-auth/blob/a12b7fc331443bdbf1cc150c6a21ec38ddbf5428/packages/better-auth/src/api/routes/session.ts#L277).
Author
Owner

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

Any updates on this issue ? There is a fix ?

<!-- gh-comment-id:3008392682 --> @quuentinho commented on GitHub (Jun 26, 2025): Any updates on this issue ? There is a fix ?
Author
Owner

@ping-maxwell commented on GitHub (Jul 2, 2025):

Any updates on this issue ? There is a fix ?

I'm not sure there is much of a fix from BA's side.
Based on the comments from above, it comes down to how you use Better-auth in your app.

If you're not using the APIs on the client it's likely cookies won't be set on the client without additional code/plugins doing so.

<!-- gh-comment-id:3026671275 --> @ping-maxwell commented on GitHub (Jul 2, 2025): > Any updates on this issue ? There is a fix ? I'm not sure there is much of a fix from BA's side. Based on the comments from above, it comes down to how you use Better-auth in your app. If you're not using the APIs on the client it's likely cookies won't be set on the client without additional code/plugins doing so.
Author
Owner

@runarsventures commented on GitHub (Jul 4, 2025):

I'm wondering if its possible to get the user without refreshing the session on the server, and then get the user normally on the client side which would refresh the session and the cookie.

Is there a way to get the user/session without the session being refreshed in the db?

I'm using Nuxt btw

<!-- gh-comment-id:3035963871 --> @runarsventures commented on GitHub (Jul 4, 2025): I'm wondering if its possible to get the user without refreshing the session on the server, and then get the user normally on the client side which would refresh the session and the cookie. Is there a way to get the user/session without the session being refreshed in the db? I'm using Nuxt btw
Author
Owner

@s-petey commented on GitHub (Jul 4, 2025):

I'm wondering if its possible to get the user without refreshing the session on the server, and then get the user normally on the client side which would refresh the session and the cookie.

Is there a way to get the user/session without the session being refreshed in the db?

I'm using Nuxt btw

@runarsventures This is currently how the server side API works. Please read the docs which defines this method, await auth.api.getSession({...}). It will return the session and user (if they are active), without updating the session. Do note you must pass the headers based on your environment (Nuxt).

<!-- gh-comment-id:3036720229 --> @s-petey commented on GitHub (Jul 4, 2025): > I'm wondering if its possible to get the user without refreshing the session on the server, and then get the user normally on the client side which would refresh the session and the cookie. > > Is there a way to get the user/session without the session being refreshed in the db? > > I'm using Nuxt btw @runarsventures This is currently how the server side API works. Please read the [docs](https://www.better-auth.com/docs/concepts/api#calling-api-endpoints-on-the-server) which defines this method, `await auth.api.getSession({...})`. It will return the session and user (if they are active), without updating the session. Do note you must pass the headers based on your environment (Nuxt).
Author
Owner

@kevinmarrec commented on GitHub (Jul 14, 2025):

I think @s-petey is right and the only piece missing is the ability to have auth.api.getSession returning headers which should contain set-cookie header with updated cookie that we can manually attach to our response headers.

I'm still unusure how it works when using useSession client side (Didn't try it, I'm not using Better Auth Client), if cookies are http only, client can't update the cookie so I think this issue is still happening in any scenario.

<!-- gh-comment-id:3069086322 --> @kevinmarrec commented on GitHub (Jul 14, 2025): I think @s-petey is right and the only piece missing is the ability to have `auth.api.getSession` returning headers which should contain `set-cookie` header with updated cookie that we can manually attach to our response headers. I'm still unusure how it works when using `useSession` client side (Didn't try it, I'm not using Better Auth Client), if cookies are http only, client can't update the cookie so I think this issue is still happening in any scenario.
Author
Owner

@kevinmarrec commented on GitHub (Jul 14, 2025):

Update : getSession does support returnHeaders option, it's not typed though.

Also, The set-cookie will be present in the returned headers, only when refreshed (updateAge triggered)

<!-- gh-comment-id:3069526380 --> @kevinmarrec commented on GitHub (Jul 14, 2025): Update : `getSession` does support `returnHeaders` option, it's not typed though. Also, The `set-cookie` will be present in the returned headers, **only when refreshed** (`updateAge` triggered)
Author
Owner

@liri2006 commented on GitHub (Jul 24, 2025):

So what would be the recommended workflow for Next with server side auth checks?

<!-- gh-comment-id:3112906001 --> @liri2006 commented on GitHub (Jul 24, 2025): So what would be the recommended workflow for Next with server side auth checks?
Author
Owner

@AndreyMay commented on GitHub (Jul 30, 2025):

I think the docs should be updated to include such a basic use-case:

  • securing the apis routes (for nextjs, cover both app and pages router)
  • propagating session cookie updates to the response headers

NextAuth has a section "Protecting Resources" which covers various cases. NextAuth method accepts res prop which allows updates the session cookie in the response internally: const session = await auth(req, res)

And then somehow BetterAuth client side should be made aware that cookie was updated (trigger cookie reload after each api call?)

<!-- gh-comment-id:3134582018 --> @AndreyMay commented on GitHub (Jul 30, 2025): I think the docs should be updated to include such a basic use-case: - securing the apis routes (for nextjs, cover both app and pages router) - propagating session cookie updates to the response headers NextAuth has a section "Protecting Resources" which covers various cases. NextAuth method accepts `res` prop which allows updates the session cookie in the response internally: `const session = await auth(req, res)` And then somehow BetterAuth client side should be made aware that cookie was updated (trigger cookie reload after each api call?)
Author
Owner

@ping-maxwell commented on GitHub (Jul 31, 2025):

So what would be the recommended workflow for Next with server side auth checks?

Could this help?
https://www.better-auth.com/docs/integrations/next#how-to-handle-auth-checks-in-each-pageroute

cc @liri2006

<!-- gh-comment-id:3138343585 --> @ping-maxwell commented on GitHub (Jul 31, 2025): > So what would be the recommended workflow for Next with server side auth checks? Could this help? https://www.better-auth.com/docs/integrations/next#how-to-handle-auth-checks-in-each-pageroute cc @liri2006
Author
Owner

@liri2006 commented on GitHub (Jul 31, 2025):

@ping-maxwell, maybe I should've written more elaborate question, as I meant "how to use server side check so that maxAge is automatically updated in the cookie". Because now new cookie is not updated, only when using getSession from client side.

<!-- gh-comment-id:3139326766 --> @liri2006 commented on GitHub (Jul 31, 2025): @ping-maxwell, maybe I should've written more elaborate question, as I meant "how to use server side check so that maxAge is automatically updated in the cookie". Because now new cookie is not updated, only when using getSession from client side.
Author
Owner

@betamasi commented on GitHub (Aug 6, 2025):

Here’s my workaround — handled it with a hook.
it ain't much but honest work

 hooks: {
      after: createAuthMiddleware(async (context) => {
        if (context.path === "/get-session") {
          const {
            context: {
              session,
              createAuthCookie,
              authCookies: { sessionToken },
            },
            getCookie,
          } = context;
          if (session) {
            const newCookie = createAuthCookie(
              sessionToken.name.split(".")[1],
              sessionToken.options,
            );
            const { expiresAt } = session.session;
            const cookieValue = getCookie(sessionToken.name);
            const setCookie = `${newCookie.name}=${cookieValue}; Expires=${expiresAt}; Path=${newCookie.attributes.path}; SameSite=${newCookie.attributes.sameSite}; ${newCookie.attributes.httpOnly ? "HttpOnly;" : ""} ${newCookie.attributes.secure ? "Secure;" : ""}`;
            context.setHeader("Set-Cookie", setCookie);
          }
        }
      }),
    },
<!-- gh-comment-id:3159872471 --> @betamasi commented on GitHub (Aug 6, 2025): Here’s my workaround — handled it with a hook. it ain't much but honest work ```typescript hooks: { after: createAuthMiddleware(async (context) => { if (context.path === "/get-session") { const { context: { session, createAuthCookie, authCookies: { sessionToken }, }, getCookie, } = context; if (session) { const newCookie = createAuthCookie( sessionToken.name.split(".")[1], sessionToken.options, ); const { expiresAt } = session.session; const cookieValue = getCookie(sessionToken.name); const setCookie = `${newCookie.name}=${cookieValue}; Expires=${expiresAt}; Path=${newCookie.attributes.path}; SameSite=${newCookie.attributes.sameSite}; ${newCookie.attributes.httpOnly ? "HttpOnly;" : ""} ${newCookie.attributes.secure ? "Secure;" : ""}`; context.setHeader("Set-Cookie", setCookie); } } }), }, ````
Author
Owner

@ArtGurianov commented on GitHub (Sep 20, 2025):

The solution for this would be to completely skip BA`s cookie caching feature in NextJS and implement your own caching provided by Next. I don't have a working solution yet, but we should look at "use cache", it will cache the request.

Then on the client if we use it with react-query, the rq`s "refetch" method won't give us fresh data since we have the value cached by Next. So every time you want to refetch the fresh value, we gotta make sure to fire tag invalidation at some previous step of mutating data (in server actions).

The ugly part is that a lot of data mutations are happening in provided BA's SDK methods, so most likely we will have to rewrite many of the BA's functionality to add revalidation of tags.

If anyone has already implemented this, please share your experience 🤖

<!-- gh-comment-id:3314545385 --> @ArtGurianov commented on GitHub (Sep 20, 2025): The solution for this would be to completely skip BA`s cookie caching feature in NextJS and implement your own caching provided by Next. I don't have a working solution yet, but we should look at "use cache", it will cache the request. Then on the client if we use it with react-query, the rq`s "refetch" method won't give us fresh data since we have the value cached by Next. So every time you want to refetch the fresh value, we gotta make sure to fire tag invalidation at some previous step of mutating data (in server actions). The ugly part is that a lot of data mutations are happening in provided BA's SDK methods, so most likely we will have to rewrite many of the BA's functionality to add revalidation of tags. If anyone has already implemented this, please share your experience 🤖
Author
Owner

@Bekacru commented on GitHub (Oct 6, 2025):

I think this issue is related to server components and other environments not being able to set cookies in time, which causes the max age to not update. We’re working on introducing a pulse mechanism that periodically hits the /get-session endpoint to refresh the cookie when it’s due, instead of relying solely on the user to call getSession correctly

<!-- gh-comment-id:3373485241 --> @Bekacru commented on GitHub (Oct 6, 2025): I think this issue is related to server components and other environments not being able to set cookies in time, which causes the max age to not update. We’re working on introducing a pulse mechanism that periodically hits the `/get-session` endpoint to refresh the cookie when it’s due, instead of relying solely on the user to call `getSession` correctly
Author
Owner

@onderonur commented on GitHub (Nov 4, 2025):

I think I found a workaround for this issue. At least, it looks like it works for my case.

So, if we summarize the problem it is basically:

  • User opens a link on our app.
  • If we have a auth.api.getSession usage in an RSC to get user info etc., and if updateAge of the session is passed, better-auth refreshes the session in DB, but can not update the cookie since RSCs can't update cookies right now.
  • At this point, even if you have a client component calling authClient.useSession hook to trigger refreshing the session, it does not work because (most of the times) a couple ms ago it was already updated in the DB and updateAge is not passed yet.

Let's say I have this config:

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: 'postgresql',
  }),
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID as string,
      clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
    },
  },
  // These values are for testing purposes.
  session: {
    expiresIn: 30,
    updateAge: 10,
  },
  plugins: [nextCookies()],
});

If we can stop RSCs from refreshing the sessions, client-side useSession hook can refresh session in DB and cookie after updateAge.

So, we need to distinguish if we get the session from an RSC or a server action. Next.js sends a Next-Action header when calling a server action. This is some internal thing of Next.js and this can change in the future. So, relying purely on this may break. But like I said, this is a workaround.

If we use the function below to get the session in RSCs and server actions, we can prevent RSCs refreshing the session in DB.

import { auth } from '@/features/auth/auth';
import { headers } from 'next/headers';
import 'server-only';

export const getSession = async () => {
  const headersList = await headers();
  const isInAction = headersList.get('Next-Action');

  const response = await auth.api.getSession({
    headers: headersList,
    query: {
      disableRefresh: !isInAction,
    },
  });

  return response;
};

And we can call useSession in some client component like below to refresh the session.

'use client';

import { authClient } from '../auth-client';

export function SessionRefresher() {
  authClient.useSession();

  return null;
}

Put <SessionRefresher /> in your root layout for example, and when user opens your app, session will be refreshed.

If you want to refresh the session when the user navigates to a new path, something like this can work:

'use client';

import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
import { authClient } from '../auth-client';

export function SessionRefresher() {
  const pathname = usePathname();

  useEffect(() => {
    authClient.getSession();
  }, [pathname]);

  return null;
}

Relying on Next-Action header might be bad since it is not a public API of Next.js and they can change it. So, you can also not use it and write your getSession function like this:

import { auth } from '@/features/auth/auth';
import { headers } from 'next/headers';
import 'server-only';

export const getSession = async ({
  disableRefresh,
}: {
  disableRefresh: boolean;
}) => {
  const headersList = await headers();

  const response = await auth.api.getSession({
    headers: headersList,
    query: {
      disableRefresh,
    },
  });

  return response;
};

If you use it, TypeScript will let you know disableRefresh is a required param and you can pass disableRefresh: true in RSCs to stop them refreshing the session.

Like I said, this is a workaround and handling this in better-auth itself would be much more better. And the solutions I propose might have issues. But since I've just tried them and seen it worked generally, I wanted to share 🙏

<!-- gh-comment-id:3487926970 --> @onderonur commented on GitHub (Nov 4, 2025): I think I found a workaround for this issue. At least, it looks like it works for my case. So, if we summarize the problem it is basically: - User opens a link on our app. - If we have a `auth.api.getSession` usage in an RSC to get user info etc., and if `updateAge` of the session is passed, `better-auth` refreshes the session in DB, but can not update the cookie since RSCs can't update cookies right now. - At this point, even if you have a client component calling `authClient.useSession` hook to trigger refreshing the session, it does not work because (most of the times) a couple `ms` ago it was already updated in the DB and `updateAge` is not passed yet. Let's say I have this config: ```ts export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: 'postgresql', }), socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID as string, clientSecret: process.env.GITHUB_CLIENT_SECRET as string, }, }, // These values are for testing purposes. session: { expiresIn: 30, updateAge: 10, }, plugins: [nextCookies()], }); ``` If we can stop RSCs from refreshing the sessions, client-side `useSession` hook can refresh session in DB and cookie after `updateAge`. So, we need to distinguish if we get the session from an RSC or a server action. Next.js sends a `Next-Action` header when calling a server action. This is some internal thing of Next.js and this can change in the future. So, relying purely on this may break. But like I said, this is a workaround. If we use the function below to get the session in RSCs and server actions, we can prevent RSCs refreshing the session in DB. ```ts import { auth } from '@/features/auth/auth'; import { headers } from 'next/headers'; import 'server-only'; export const getSession = async () => { const headersList = await headers(); const isInAction = headersList.get('Next-Action'); const response = await auth.api.getSession({ headers: headersList, query: { disableRefresh: !isInAction, }, }); return response; }; ``` And we can call `useSession` in some client component like below to refresh the session. ```tsx 'use client'; import { authClient } from '../auth-client'; export function SessionRefresher() { authClient.useSession(); return null; } ``` Put `<SessionRefresher />` in your root layout for example, and when user opens your app, session will be refreshed. If you want to refresh the session when the user navigates to a new path, something like this can work: ```tsx 'use client'; import { usePathname } from 'next/navigation'; import { useEffect } from 'react'; import { authClient } from '../auth-client'; export function SessionRefresher() { const pathname = usePathname(); useEffect(() => { authClient.getSession(); }, [pathname]); return null; } ``` Relying on `Next-Action` header might be bad since it is not a public API of Next.js and they can change it. So, you can also not use it and write your `getSession` function like this: ```ts import { auth } from '@/features/auth/auth'; import { headers } from 'next/headers'; import 'server-only'; export const getSession = async ({ disableRefresh, }: { disableRefresh: boolean; }) => { const headersList = await headers(); const response = await auth.api.getSession({ headers: headersList, query: { disableRefresh, }, }); return response; }; ``` If you use it, TypeScript will let you know `disableRefresh` is a required param and you can pass `disableRefresh: true` in RSCs to stop them refreshing the session. Like I said, this is a workaround and handling this in `better-auth` itself would be much more better. And the solutions I propose might have issues. But since I've just tried them and seen it worked generally, I wanted to share 🙏
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 4, 2025):

tag @better-auth-agent

Hi there! It looks like you've shared a detailed description along with a workaround regarding the session cookie not being refreshed when using RSCs. Based on your report, the issue arises because when a session is refreshed on the backend (after the updateAge), the session cookie's maxAge isn't updated, effectively logging out the user despite the database being refreshed. This is particularly observable in setups where RSCs fetch the session, and Next.js's internal header (like "Next-Action") is used to determine if the session refresh should be disabled.

Your workaround of distinguishing between server actions and RSCs by checking for the "Next-Action" header is a valid approach given the current constraints with RSCs not being able to update cookies. Additionally, providing a version of your getSession function that requires an explicit disableRefresh flag in RSCs is a smart way to avoid unintended refreshes. This lets the client component's useSession hook handle updating both the session in the database and the cookie, ensuring the session is extended as expected [1].

The next steps would be:

  1. Confirm that your client-side SessionRefresher component is consistently rendering on key navigations and that calling authClient.getSession() from there reliably updates the cookie.
  2. Monitor for any updates from Next.js regarding cookie updates within RSCs, as future changes might allow a more integrated solution without having to rely on the "Next-Action" header.
  3. Consider opening an issue or discussion in our repository to share this scenario. It could help shape a built-in solution within Better Auth for distinguishing between RSC and client-side session refreshes.

Does this address your concerns, or is there another aspect of the session refresh behavior you’d like to dig into further?

tag @better-auth-agent

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3487928118 --> @better-auth-agent[bot] commented on GitHub (Nov 4, 2025): tag @better-auth-agent Hi there! It looks like you've shared a detailed description along with a workaround regarding the session cookie not being refreshed when using RSCs. Based on your report, the issue arises because when a session is refreshed on the backend (after the updateAge), the session cookie's maxAge isn't updated, effectively logging out the user despite the database being refreshed. This is particularly observable in setups where RSCs fetch the session, and Next.js's internal header (like "Next-Action") is used to determine if the session refresh should be disabled. Your workaround of distinguishing between server actions and RSCs by checking for the "Next-Action" header is a valid approach given the current constraints with RSCs not being able to update cookies. Additionally, providing a version of your getSession function that requires an explicit disableRefresh flag in RSCs is a smart way to avoid unintended refreshes. This lets the client component's useSession hook handle updating both the session in the database and the cookie, ensuring the session is extended as expected [[1]](https://github.com/better-auth/better-auth/issues/4125). The next steps would be: 1. Confirm that your client-side SessionRefresher component is consistently rendering on key navigations and that calling authClient.getSession() from there reliably updates the cookie. 2. Monitor for any updates from Next.js regarding cookie updates within RSCs, as future changes might allow a more integrated solution without having to rely on the "Next-Action" header. 3. Consider opening an issue or discussion in our repository to share this scenario. It could help shape a built-in solution within Better Auth for distinguishing between RSC and client-side session refreshes. Does this address your concerns, or is there another aspect of the session refresh behavior you’d like to dig into further? tag @better-auth-agent <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 4, 2025):

Hi there! Based on your description, it appears that the session is being refreshed in the database yet the session cookie isn’t updated because the refresh is triggered from a React Server Component (RSC), which can’t update cookies at runtime. This limitation is known, and a workaround is to explicitly prevent RSCs (or server actions) from refreshing the session. Instead, the session refresh should be handled client‑side, where cookies can be updated.

Here are the next steps to try:

  1. Use a modified getSession function that accepts a disableRefresh parameter (as you demonstrated) so that RSC calls don’t trigger the session update. This ensures that only client‑side calls (e.g., via authClient.useSession) perform the refresh.
  2. Place a dedicated client component (like your SessionRefresher) in your root layout to call authClient.useSession and trigger an update of both the DB session and the cookie.
  3. Monitor whether the session cookie’s expiry now reflects the updated session’s expiresIn value as intended.

This approach is a temporary workaround until better-auth can more seamlessly coordinate RSC and client-side session refreshes. Does this solution help address the issue in your app? tag @better-auth-agent

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3487929564 --> @better-auth-agent[bot] commented on GitHub (Nov 4, 2025): Hi there! Based on your description, it appears that the session is being refreshed in the database yet the session cookie isn’t updated because the refresh is triggered from a React Server Component (RSC), which can’t update cookies at runtime. This limitation is known, and a workaround is to explicitly prevent RSCs (or server actions) from refreshing the session. Instead, the session refresh should be handled client‑side, where cookies can be updated. Here are the next steps to try: 1. Use a modified getSession function that accepts a disableRefresh parameter (as you demonstrated) so that RSC calls don’t trigger the session update. This ensures that only client‑side calls (e.g., via authClient.useSession) perform the refresh. 2. Place a dedicated client component (like your SessionRefresher) in your root layout to call authClient.useSession and trigger an update of both the DB session and the cookie. 3. Monitor whether the session cookie’s expiry now reflects the updated session’s expiresIn value as intended. This approach is a temporary workaround until better-auth can more seamlessly coordinate RSC and client-side session refreshes. Does this solution help address the issue in your app? tag @better-auth-agent <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 4, 2025):

Hi there, thanks for the detailed description of your workaround. It appears that the root issue is that when the session is refreshed from a server component (or a server action), the update to the session expiration in the database doesn’t propagate to update the cookie itself, due to limitations on updating cookies in React Server Components. This is why even though the session in the database is extended, the user still gets logged out when the cookie expires.

Your solution of checking for the Next-Action header and disabling refresh in RSCs—then relying on a client component (using useSession or calling getSession directly) to properly refresh the session cookie—is a sensible temporary workaround [1]. In some cases, avoiding automatic server-side refresh and instead letting the client explicitly trigger a session refresh ensures the cookie is updated with the new expiry [2].

Next steps could include:
• Verifying that the client-side component (such as a global session refresher) is effectively calling getSession after navigation changes.
• Watching for any Next.js updates regarding public APIs for distinguishing server actions from server components since the Next-Action header is not a public API and may change.
• If possible, testing alternative configurations (or tracking future releases of better‑auth) where handling cookie refresh on server actions is improved.

Does this approach solve your immediate problem, or are there additional aspects you’d like to troubleshoot?
tag @better-auth-agent

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3487943692 --> @better-auth-agent[bot] commented on GitHub (Nov 4, 2025): Hi there, thanks for the detailed description of your workaround. It appears that the root issue is that when the session is refreshed from a server component (or a server action), the update to the session expiration in the database doesn’t propagate to update the cookie itself, due to limitations on updating cookies in React Server Components. This is why even though the session in the database is extended, the user still gets logged out when the cookie expires. Your solution of checking for the Next-Action header and disabling refresh in RSCs—then relying on a client component (using useSession or calling getSession directly) to properly refresh the session cookie—is a sensible temporary workaround [[1]](https://github.com/better-auth/better-auth/issues/4125). In some cases, avoiding automatic server-side refresh and instead letting the client explicitly trigger a session refresh ensures the cookie is updated with the new expiry [[2]](https://github.com/better-auth/better-auth/issues/2139). Next steps could include: • Verifying that the client-side component (such as a global session refresher) is effectively calling getSession after navigation changes. • Watching for any Next.js updates regarding public APIs for distinguishing server actions from server components since the Next-Action header is not a public API and may change. • If possible, testing alternative configurations (or tracking future releases of better‑auth) where handling cookie refresh on server actions is improved. Does this approach solve your immediate problem, or are there additional aspects you’d like to troubleshoot? tag @better-auth-agent <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@dufia commented on GitHub (Nov 10, 2025):

I am using tasntack start and indeed had the same problem. I disabled server function refresh and added a client side component to check and refresh periodically.

export const getSessionFn = createServerFn({ method: "GET" }).handler(async () => {
  const headers = getRequestHeaders();
  const session = await auth.api.getSession({
    headers,
    query: {
      disableRefresh: true,
    },
  });

  return { session };
});
unction SessionRefresher() {
  const { data, refetch } = authClient.useSession();

  useInterval(() => {
    if (!data?.session) {
      return;
    }

    const { createdAt } = data.session;
    const oneDayAfter = addDays(createdAt, 1);
    const hasOneDayPassed = isAfter(new Date(), oneDayAfter);

    if (hasOneDayPassed) {
      authClient.getSession({
        fetchOptions: {
          onSuccess: () => {
            console.log("session refreshed");
            refetch();
          },
        },
      });
    }
  }, 5000);

  return null;
}
<!-- gh-comment-id:3513385974 --> @dufia commented on GitHub (Nov 10, 2025): I am using tasntack start and indeed had the same problem. I disabled server function refresh and added a client side component to check and refresh periodically. ``` export const getSessionFn = createServerFn({ method: "GET" }).handler(async () => { const headers = getRequestHeaders(); const session = await auth.api.getSession({ headers, query: { disableRefresh: true, }, }); return { session }; }); ``` ``` unction SessionRefresher() { const { data, refetch } = authClient.useSession(); useInterval(() => { if (!data?.session) { return; } const { createdAt } = data.session; const oneDayAfter = addDays(createdAt, 1); const hasOneDayPassed = isAfter(new Date(), oneDayAfter); if (hasOneDayPassed) { authClient.getSession({ fetchOptions: { onSuccess: () => { console.log("session refreshed"); refetch(); }, }, }); } }, 5000); return null; } ```
Author
Owner

@pranavgoel29 commented on GitHub (Jan 27, 2026):

+1 for auth.api.refreshCookies method so we can refresh the cookies even when only using server side implementation for better auth like HONO

<!-- gh-comment-id:3806404928 --> @pranavgoel29 commented on GitHub (Jan 27, 2026): +1 for `auth.api.refreshCookies` method so we can refresh the cookies even when only using server side implementation for better auth like HONO
Author
Owner

@heliohm commented on GitHub (Jan 29, 2026):

Not Next.js - but here is an example for how to achieve server side use of better auth with React Router v7 and session refreshes. The trick is that you need to forward the Set-Cookie header from better auth so that it is received by the browser.

When using the client, the client already runs in the browser. When better-fetch is used towards the server side API at /api/auth/*, the "Set-Cookie" header reaches the browser directly from within the library. Thus, when the client is not used, you need to forward those headers yourself.

My middleware:

// packages/core/src/auth/middleware.ts

import { createContext, redirect, type MiddlewareFunction } from "react-router";

import { Auth } from "./index.js";
import type { OptionalAuthSession, AuthSession } from "./types.js";

type OptionalAuthSessionContext = {
  authSession: OptionalAuthSession;
  authHeaders?: Headers;
};
type AuthSessionContext = {
  authSession: AuthSession;
  authHeaders?: Headers;
};

export const optionalAuthSessionContext =
  createContext<OptionalAuthSessionContext>();

export const authSessionContext = createContext<AuthSessionContext>();

/**
 * Fetches the current session with headers.
 * Returns session data and any Set-Cookie headers if the session was refreshed.
 */
async function fetchSessionWithHeaders(request: Request) {
  const response = await Auth.get().api.getSession({
    headers: request.headers,
    asResponse: true,
  });

  const session = response ? await response.json() : null;

  return {
    session,
    headers: response?.headers,
  };
}

export const optionalAuthSession: MiddlewareFunction = async (
  { context, request },
  _,
) => {
  const { session, headers } = await fetchSessionWithHeaders(request);

  const authSessionValue: OptionalAuthSession = session
    ? { user: session.user }
    : null;

  context.set(optionalAuthSessionContext, {
    authSession: authSessionValue,
    authHeaders: headers,
  });
};

function requireAuthSession(
  signInPath: string,
  redirectPath: string,
): MiddlewareFunction {
  const redirectTarget = buildSignInRedirect(signInPath, redirectPath);
  return async ({ context, request }, _) => {
    const { session, headers } = await fetchSessionWithHeaders(request);

    if (!session) {
      throw redirect(redirectTarget);
    }

    const authSessionValue: AuthSession = { user: session.user };

    context.set(authSessionContext, {
      authSession: authSessionValue,
      authHeaders: headers,
    });
  };
}

export function requireAuthSessionRedirectToRoot(
  signInPath: string,
): MiddlewareFunction {
  return requireAuthSession(signInPath, "/");
}

export function requireAuthSessionRedirectToCurrentPath(
  signInPath: string,
): MiddlewareFunction {
  return async ({ context, request }, _) => {
    const { session, headers } = await fetchSessionWithHeaders(request);

    if (!session) {
      const currentPath = new URL(request.url).pathname;
      const redirectTarget = buildSignInRedirect(signInPath, currentPath);
      throw redirect(redirectTarget);
    }

    const authSessionValue: AuthSession = { user: session.user };

    context.set(authSessionContext, {
      authSession: authSessionValue,
      authHeaders: headers,
    });
  };
}

function buildSignInRedirect(signInPath: string, redirectPath: string): string {
  const params = new URLSearchParams({ redirectTo: redirectPath });
  return `${signInPath}?${params.toString()}`;
}

// Routes - this could also be moved to a layout route wrapping all protected pages. Once you access a protected page, the cookie is refreshed

// apps/my-app/app/protected-route.ts
const signInPath = `/${authRoute}/sign-in`;

export const middleware = [requireAuthSessionRedirectToCurrentPath(signInPath)];

export const loader = ({ context }: Route.LoaderArgs) => {
  const auth = context.get(authSessionContext);

  const loaderData = { auth };

  // Propagate auth headers if session was refreshed
  if (auth.authHeaders) {
    return data(loaderData, {
      headers: auth.authHeaders,
    });
  }

  return loaderData;
};

export function headers({ loaderHeaders }: Route.HeadersArgs) {
  return loaderHeaders;
}rwad
<!-- gh-comment-id:3815970844 --> @heliohm commented on GitHub (Jan 29, 2026): Not Next.js - but here is an example for how to achieve server side use of better auth with React Router v7 and session refreshes. The trick is that you need to forward the Set-Cookie header from better auth so that it is received by the browser. When using the client, the client already runs in the browser. When better-fetch is used towards the server side API at /api/auth/*, the "Set-Cookie" header reaches the browser directly from within the library. Thus, when the client is not used, you need to forward those headers yourself. My middleware: ``` // packages/core/src/auth/middleware.ts import { createContext, redirect, type MiddlewareFunction } from "react-router"; import { Auth } from "./index.js"; import type { OptionalAuthSession, AuthSession } from "./types.js"; type OptionalAuthSessionContext = { authSession: OptionalAuthSession; authHeaders?: Headers; }; type AuthSessionContext = { authSession: AuthSession; authHeaders?: Headers; }; export const optionalAuthSessionContext = createContext<OptionalAuthSessionContext>(); export const authSessionContext = createContext<AuthSessionContext>(); /** * Fetches the current session with headers. * Returns session data and any Set-Cookie headers if the session was refreshed. */ async function fetchSessionWithHeaders(request: Request) { const response = await Auth.get().api.getSession({ headers: request.headers, asResponse: true, }); const session = response ? await response.json() : null; return { session, headers: response?.headers, }; } export const optionalAuthSession: MiddlewareFunction = async ( { context, request }, _, ) => { const { session, headers } = await fetchSessionWithHeaders(request); const authSessionValue: OptionalAuthSession = session ? { user: session.user } : null; context.set(optionalAuthSessionContext, { authSession: authSessionValue, authHeaders: headers, }); }; function requireAuthSession( signInPath: string, redirectPath: string, ): MiddlewareFunction { const redirectTarget = buildSignInRedirect(signInPath, redirectPath); return async ({ context, request }, _) => { const { session, headers } = await fetchSessionWithHeaders(request); if (!session) { throw redirect(redirectTarget); } const authSessionValue: AuthSession = { user: session.user }; context.set(authSessionContext, { authSession: authSessionValue, authHeaders: headers, }); }; } export function requireAuthSessionRedirectToRoot( signInPath: string, ): MiddlewareFunction { return requireAuthSession(signInPath, "/"); } export function requireAuthSessionRedirectToCurrentPath( signInPath: string, ): MiddlewareFunction { return async ({ context, request }, _) => { const { session, headers } = await fetchSessionWithHeaders(request); if (!session) { const currentPath = new URL(request.url).pathname; const redirectTarget = buildSignInRedirect(signInPath, currentPath); throw redirect(redirectTarget); } const authSessionValue: AuthSession = { user: session.user }; context.set(authSessionContext, { authSession: authSessionValue, authHeaders: headers, }); }; } function buildSignInRedirect(signInPath: string, redirectPath: string): string { const params = new URLSearchParams({ redirectTo: redirectPath }); return `${signInPath}?${params.toString()}`; } ``` // Routes - this could also be moved to a layout route wrapping all protected pages. Once you access a protected page, the cookie is refreshed ``` // apps/my-app/app/protected-route.ts const signInPath = `/${authRoute}/sign-in`; export const middleware = [requireAuthSessionRedirectToCurrentPath(signInPath)]; export const loader = ({ context }: Route.LoaderArgs) => { const auth = context.get(authSessionContext); const loaderData = { auth }; // Propagate auth headers if session was refreshed if (auth.authHeaders) { return data(loaderData, { headers: auth.authHeaders, }); } return loaderData; }; export function headers({ loaderHeaders }: Route.HeadersArgs) { return loaderHeaders; }rwad
Author
Owner

@heiwen commented on GitHub (Jan 29, 2026):

Thanks for sharing @heliohm. Would be great to see that example in the better-auth documentation or, even better, as a React Router integration package.

<!-- gh-comment-id:3816079667 --> @heiwen commented on GitHub (Jan 29, 2026): Thanks for sharing @heliohm. Would be great to see that example in the better-auth documentation or, even better, as a React Router integration package.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9057