[GH-ISSUE #8059] Session token is not set in cookies for Tanstack Start + Drizzle + Turbo MonoRepo setup #10983

Open
opened 2026-04-13 07:22:05 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @harshtalks on GitHub (Feb 19, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8059

Originally assigned to: @bytaesu on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I'm trying to integrate better auth with my tanstack app in a turbo monorepo setup, but for some reason session token is never present in the cookies. Because of that I'm always getting null in the session response.

Current vs. Expected behavior

Current: No cookie for session token present in the headers
Expected: Valid cookie header should be set

What version of Better Auth are you using?

1.4.18

System info

NA

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

Client

Auth config (if applicable)

import { createServerOnlyFn } from "@tanstack/react-start";
import db from "@typemate/db";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { betterAuth } from "better-auth/minimal";
import { magicLink } from "better-auth/plugins";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { Resend } from "resend";
import { envs } from "../env";

const resend = new Resend(envs.SERVER_RESEND_API_KEY);

export const getAuthConfig = createServerOnlyFn(() =>
  betterAuth({
    secret: envs.SERVER_BETTER_AUTH_SECRET,
    baseURL: envs.SERVER_BETTER_AUTH_BASE_URL,
    database: drizzleAdapter(db, { provider: "sqlite" }),
    telemetry: {
      debug: true,
      enabled: true,
    },
    plugins: [
      magicLink({
        sendMagicLink: async ({ url, email }) => {
          const response = await resend.emails.send({
            to: email,
            subject: "Sign in to your account",
            html: `<h1>Sign in to your account</h1><p>Click the link below to sign in to your account:</p><p><a href="${url}">${url}</a></p>`,
            text: `Sign in to your account\n\nClick the link below to sign in to your account:\n\n${url}`,
            from: "Acme <onboarding@resend.dev>",
          });

          if (response.error) {
            throw new Error(response.error.message);
          }
        },
      }),
      tanstackStartCookies(),
    ],
    session: {
      cookieCache: {
        enabled: true,
        maxAge: 5 * 60, // 5 minutes
        refreshCache: true,
        strategy: "jwt",
      },
    },
    socialProviders: {
      github: {
        clientId: envs.SERVER_GITHUB_CLIENT_ID,
        clientSecret: envs.SERVER_GITHUB_CLIENT_SECRET,
      },
    },
    onAPIError: {
      throw: true,
    },
    advanced: {
      database: {
        generateId: false,
      },
      cookiePrefix: "typemate",
    },
  })
);

export const auth = getAuthConfig();

Additional context

Schema for session table

import type { Branded } from "@typemate/types";
import {
  type InferInsertModel,
  type InferSelectModel,
  relations,
} from "drizzle-orm";
import * as t from "drizzle-orm/sqlite-core";
import {
  createdAtSchema,
  createTimestampSchema,
  updatedAtSchema,
} from "../schema.common";
import { PrefixedIDs } from "../schema.helper";
import { user } from "../user";

export const session = t.sqliteTable("session", {
  id: t
    .text("id")
    .primaryKey()
    .$defaultFn(PrefixedIDs.session)
    .$type<Branded.SessionId>(),
  token: t.text("token").notNull(),
  ipAddress: t.text("ip_address"),
  userAgent: t.text("user_agent"),
  expiresAt: createTimestampSchema("expires_at"),
  createdAt: createdAtSchema,
  updatedAt: updatedAtSchema,
  // foreign key - user
  userId: t
    .text("user_id")
    .notNull()
    .references(() => user.id, {
      onDelete: "cascade",
    })
    .$type<Branded.UserId>(),
});

export const sessionRelations = relations(session, ({ one }) => ({
  user: one(user),
}));

export type Session = InferSelectModel<typeof session>;
export type SessionInsert = InferInsertModel<typeof session>;
Originally created by @harshtalks on GitHub (Feb 19, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8059 Originally assigned to: @bytaesu on GitHub. ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce I'm trying to integrate better auth with my tanstack app in a turbo monorepo setup, but for some reason session token is never present in the cookies. Because of that I'm always getting null in the session response. ### Current vs. Expected behavior Current: No cookie for session token present in the headers Expected: Valid cookie header should be set ### What version of Better Auth are you using? 1.4.18 ### System info ```bash NA ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import { createServerOnlyFn } from "@tanstack/react-start"; import db from "@typemate/db"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { betterAuth } from "better-auth/minimal"; import { magicLink } from "better-auth/plugins"; import { tanstackStartCookies } from "better-auth/tanstack-start"; import { Resend } from "resend"; import { envs } from "../env"; const resend = new Resend(envs.SERVER_RESEND_API_KEY); export const getAuthConfig = createServerOnlyFn(() => betterAuth({ secret: envs.SERVER_BETTER_AUTH_SECRET, baseURL: envs.SERVER_BETTER_AUTH_BASE_URL, database: drizzleAdapter(db, { provider: "sqlite" }), telemetry: { debug: true, enabled: true, }, plugins: [ magicLink({ sendMagicLink: async ({ url, email }) => { const response = await resend.emails.send({ to: email, subject: "Sign in to your account", html: `<h1>Sign in to your account</h1><p>Click the link below to sign in to your account:</p><p><a href="${url}">${url}</a></p>`, text: `Sign in to your account\n\nClick the link below to sign in to your account:\n\n${url}`, from: "Acme <onboarding@resend.dev>", }); if (response.error) { throw new Error(response.error.message); } }, }), tanstackStartCookies(), ], session: { cookieCache: { enabled: true, maxAge: 5 * 60, // 5 minutes refreshCache: true, strategy: "jwt", }, }, socialProviders: { github: { clientId: envs.SERVER_GITHUB_CLIENT_ID, clientSecret: envs.SERVER_GITHUB_CLIENT_SECRET, }, }, onAPIError: { throw: true, }, advanced: { database: { generateId: false, }, cookiePrefix: "typemate", }, }) ); export const auth = getAuthConfig(); ``` ### Additional context Schema for session table ``` import type { Branded } from "@typemate/types"; import { type InferInsertModel, type InferSelectModel, relations, } from "drizzle-orm"; import * as t from "drizzle-orm/sqlite-core"; import { createdAtSchema, createTimestampSchema, updatedAtSchema, } from "../schema.common"; import { PrefixedIDs } from "../schema.helper"; import { user } from "../user"; export const session = t.sqliteTable("session", { id: t .text("id") .primaryKey() .$defaultFn(PrefixedIDs.session) .$type<Branded.SessionId>(), token: t.text("token").notNull(), ipAddress: t.text("ip_address"), userAgent: t.text("user_agent"), expiresAt: createTimestampSchema("expires_at"), createdAt: createdAtSchema, updatedAt: updatedAtSchema, // foreign key - user userId: t .text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade", }) .$type<Branded.UserId>(), }); export const sessionRelations = relations(session, ({ one }) => ({ user: one(user), })); export type Session = InferSelectModel<typeof session>; export type SessionInsert = InferInsertModel<typeof session>; ```
GiteaMirror added the platformneeds: infodatabasecorebug labels 2026-04-13 07:22:05 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Feb 19, 2026):

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

This is a known issue with TanStack Start - it doesn't automatically forward Set-Cookie headers from server responses to the browser. A few things to check:

1. Plugin Ordering - tanstackStartCookies() must be the last plugin in your array. In your config, move it after magicLink():

plugins: [
  magicLink({ ... }),
  tanstackStartCookies() // must be last
]

2. Cookie Cache Issue - There's a known issue where enabling cookie cache with TanStack Start can cause only session_data cookies to be created while session_token is missing. Try temporarily disabling cookie cache to test:

session: {
  cookieCache: {
    enabled: false, // test with this disabled
  }
}

Or add disableCookieCache: true to your session calls:

auth.api.getSession({ headers, query: { disableCookieCache: true } })

3. Manual Header Forwarding - If cookies still aren't set, you may need to manually forward Set-Cookie headers in your middleware:

const response = await auth.api.getSession({ headers, asResponse: true });
const setCookieHeaders = response.headers.getSetCookie();
for (const cookie of setCookieHeaders) {
  responseHeaders.append('set-cookie', cookie);
}

4. Monorepo Consideration - Better Auth in monorepos can have module resolution issues if it gets bundled multiple times. Ensure better-auth is not duplicated across workspace packages and check your bundler's external/SSR optimization settings.

Also verify your API handler at /api/auth/* is correctly set up per the TanStack integration docs.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3925040321 --> @dosubot[bot] commented on GitHub (Feb 19, 2026): <!-- Greeting --> Hi @harshtalks! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known issue with TanStack Start - it doesn't automatically forward `Set-Cookie` headers from server responses to the browser. A few things to check: **1. Plugin Ordering** - `tanstackStartCookies()` must be the **last plugin** in your array. In your config, move it after `magicLink()`: ```ts plugins: [ magicLink({ ... }), tanstackStartCookies() // must be last ] ``` **2. Cookie Cache Issue** - There's a [known issue](https://github.com/better-auth/better-auth/issues/7008#issuecomment-3883529393) where enabling cookie cache with TanStack Start can cause only `session_data` cookies to be created while `session_token` is missing. Try temporarily disabling cookie cache to test: ```ts session: { cookieCache: { enabled: false, // test with this disabled } } ``` Or add `disableCookieCache: true` to your session calls: ```ts auth.api.getSession({ headers, query: { disableCookieCache: true } }) ``` **3. Manual Header Forwarding** - If cookies still aren't set, you may need to [manually forward Set-Cookie headers](https://github.com/better-auth/better-auth/issues/7891#issuecomment-3877188558) in your middleware: ```ts const response = await auth.api.getSession({ headers, asResponse: true }); const setCookieHeaders = response.headers.getSetCookie(); for (const cookie of setCookieHeaders) { responseHeaders.append('set-cookie', cookie); } ``` **4. Monorepo Consideration** - Better Auth in monorepos can have [module resolution issues](https://github.com/better-auth/better-auth/issues/7023) if it gets bundled multiple times. Ensure `better-auth` is not duplicated across workspace packages and check your bundler's external/SSR optimization settings. Also verify your API handler at `/api/auth/*` is correctly set up per the [TanStack integration docs](https://github.com/better-auth/better-auth/blob/07d80082fce33871f543f5d5191cfbf0b22adb21/docs/content/docs/integrations/tanstack.mdx#L26-L44). <!-- 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/f6db3ef3-145d-4341-ba2a-7da8c1ed715f?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/f6db3ef3-145d-4341-ba2a-7da8c1ed715f?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/f6db3ef3-145d-4341-ba2a-7da8c1ed715f?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/f6db3ef3-145d-4341-ba2a-7da8c1ed715f?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/f6db3ef3-145d-4341-ba2a-7da8c1ed715f?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/f6db3ef3-145d-4341-ba2a-7da8c1ed715f?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/f6db3ef3-145d-4341-ba2a-7da8c1ed715f?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20known%20issue%20with%20TanStack%20Start%20-%20it%20doesn%27t%20automatically%20forward%20%60Set-Cookie%60%20headers%20from%20server%20responses%20to%20the%20browser.%20A%20few%20things%20to%20check%3A%0A%0A%2A%2A1.%20Plugin%20Ordering%2A%2A%20-%20%60tanstackStartCookies%28%29%60%20must%20be%20the%20%2A%2Alast%20plugin%2A%2A%20in%20your%20array.%20In%20your%20config%2C%20move%20it%20after%20%60magicLink%28%29%60%3A%0A%60%60%60ts%0Aplugins%3A%20%5B%0A%20%20magicLink%28%7B%20...%20%7D%29%2C%0A%20%20tanstackStartCookies%28%29%20//%20must%20be%20last%0A%5D%0A%60%60%60%0A%0A%2A%2A2.%20Cookie%20Cache%20Issue%2A%2A%20-%20There%27s%20a%20%5Bknown%20issue%5D%28https%3A//github.com/better-auth/better-auth/issues/7008%23issuecomment-3883529393%29%20where%20enabling%20cookie%20cache%20with%20TanStack%20Start%20can%20cause%20only%20%60session_data%60%20cookies%20to%20be%20created%20while%20%60session_token%60%20is%20missing.%20Try%20temporarily%20disabling%20cookie%20cache%20to%20test%3A%0A%60%60%60ts%0Asession%3A%20%7B%0A%20%20cookieCache%3A%20%7B%0A%20%20%20%20enabled%3A%20false%2C%20//%20test%20with%20this%20disabled%0A%20%20%7D%0A%7D%0A%60%60%60%0AOr%20add%20%60disableCookieCache%3A%20true%60%20to%20your%20session%20calls%3A%0A%60%60%60ts%0Aauth.api.getSession%28%7B%20headers%2C%20query%3A%20%7B%20disableCookieCache%3A%20true%20%7D%20%7D%29%0A%60%60%60%0A%0A%2A%2A3.%20Manual%20Header%20Forwarding%2A%2A%20-%20If%20cookies%20still%20aren%27t%20set%2C%20you%20may%20need%20to%20%5Bmanually%20forward%20Set-Cookie%20headers%5D%28https%3A//github.com/better-auth/better-auth/issues/7891%23issuecomment-3877188558%29%20in%20your%20middleware%3A%0A%60%60%60ts%0Aconst%20response%20%3D%20await%20auth.api.getSession%28%7B%20headers%2C%20asResponse%3A%20true%20%7D%29%3B%0Aconst%20setCookieHeaders%20%3D%20response.headers.getSetCookie%28%29%3B%0Afor%20%28const%20cookie%20of%20setCookieHeaders%29%20%7B%0A%20%20responseHeaders.append%28%27set-cookie%27%2C%20cookie%29%3B%0A%7D%0A%60%60%60%0A%0A%2A%2A4.%20Monorepo%20Consideration%2A%2A%20-%20Better%20Auth%20in%20monorepos%20can%20have%20%5Bmodule%20resolution%20issues%5D%28https%3A//github.com/better-auth/better-auth/issues/7023%29%20if%20it%20gets%20bundled%20multiple%20times.%20Ensure%20%60better-auth%60%20is%20not%20duplicated%20across%20workspace%20packages%20and%20check%20your%20bundler%27s%20external/SSR%20optimization%20settings.%0A%0AAlso%20verify%20your%20API%20handler%20at%20%60/api/auth/%2A%60%20is%20correctly%20set%20up%20per%20the%20%5BTanStack%20integration%20docs%5D%28https%3A//github.com/better-auth/better-auth/blob/07d80082fce33871f543f5d5191cfbf0b22adb21/docs/content/docs/integrations/tanstack.mdx%23L26-L44%29.)&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/8059)
Author
Owner

@bytaesu commented on GitHub (Feb 22, 2026):

Hi @harshtalks,

I tested, but I couldn't reproduce this issue on my end.

Drizzle schema looks different from the one generated by @better-auth/cli generate, and it seems the schema is not being passed into the drizzleAdapter 🤔

Since this setup differs from the guide we provide, it's a bit difficult to verify the issue. Could you confirm whether it works when configured according to the guide below?

<!-- gh-comment-id:3940146917 --> @bytaesu commented on GitHub (Feb 22, 2026): Hi @harshtalks, I tested, but I couldn't reproduce this issue on my end. Drizzle schema looks different from the one generated by `@better-auth/cli generate`, and it seems the schema is not being passed into the drizzleAdapter 🤔 Since this setup differs from the guide we provide, it's a bit difficult to verify the issue. Could you confirm whether it works when configured according to the guide below? - https://www.better-auth.com/docs/integrations/tanstack - https://www.better-auth.com/docs/adapters/drizzle
Author
Owner

@ajxbit commented on GitHub (Feb 25, 2026):

@bytaesu had the same issue and from Claude

Both setCookie and setSignedCookie in better-call correctly use headers.append("set-cookie", cookie) (lines 53, 58). The cookies are added correctly to the internal Headers. But then toResponse() clones them with new Headers(init?.headers) (line 64), and this clone step is likely dropping the second set-cookie in the Wrangler/Miniflare runtime.

I traced this to better-call@1.1.8 (used by better-auth@1.4.18).

The bug

In better-call/dist/to-response.mjs line 64:

let headers = new Headers(init?.headers);

When the sign-in handler runs, setSignedCookie and setCookie in better-call/dist/context.mjs correctly use headers.append("set-cookie", cookie) to add both session_token and session_data to the internal Headers object.

But when toResponse() builds the final Response, new Headers(init?.headers) clones the headers — and in some runtimes (Wrangler/Miniflare/workerd), this clone drops duplicate set-cookie entries, keeping only the last one (session_data).

Result

The sign-in response only contains one Set-Cookie:

HTTP/1.1 200 OK
set-cookie: better-auth.session_data=eyJ...; Max-Age=300; Path=/; HttpOnly; SameSite=Lax

No session_token. Since getSession() requires session_token (returns null immediately without it), the session breaks as soon as the 5-minute session_data cache expires.

Fix suggestion

In toResponse(), use headers.append() for set-cookie instead of relying on new Headers() clone:

const headers = new Headers();
if (init?.headers) {
  const source = new Headers(init.headers);
  // Preserve individual set-cookie values
  for (const cookie of source.getSetCookie()) {
    headers.append('set-cookie', cookie);
  }
  for (const [key, value] of source.entries()) {
    if (key !== 'set-cookie') headers.set(key, value);
  }
}

Workaround

Disable cookieCache so only session_token is set (no competing session_data to overwrite it):

session: {
  cookieCache: {
    enabled: false,
  },
},
<!-- gh-comment-id:3962144257 --> @ajxbit commented on GitHub (Feb 25, 2026): @bytaesu had the same issue and from Claude ``` Both setCookie and setSignedCookie in better-call correctly use headers.append("set-cookie", cookie) (lines 53, 58). The cookies are added correctly to the internal Headers. But then toResponse() clones them with new Headers(init?.headers) (line 64), and this clone step is likely dropping the second set-cookie in the Wrangler/Miniflare runtime. ``` ## Root Cause: `better-call`'s `toResponse()` drops multiple `Set-Cookie` headers I traced this to `better-call@1.1.8` (used by `better-auth@1.4.18`). ### The bug In `better-call/dist/to-response.mjs` line 64: ```js let headers = new Headers(init?.headers); ``` When the sign-in handler runs, `setSignedCookie` and `setCookie` in `better-call/dist/context.mjs` correctly use `headers.append("set-cookie", cookie)` to add both `session_token` and `session_data` to the internal Headers object. But when `toResponse()` builds the final `Response`, `new Headers(init?.headers)` clones the headers — and **in some runtimes (Wrangler/Miniflare/workerd), this clone drops duplicate `set-cookie` entries**, keeping only the last one (`session_data`). ### Result The sign-in response only contains one `Set-Cookie`: ``` HTTP/1.1 200 OK set-cookie: better-auth.session_data=eyJ...; Max-Age=300; Path=/; HttpOnly; SameSite=Lax ``` No `session_token`. Since `getSession()` requires `session_token` (returns `null` immediately without it), the session breaks as soon as the 5-minute `session_data` cache expires. ### Fix suggestion In `toResponse()`, use `headers.append()` for `set-cookie` instead of relying on `new Headers()` clone: ```js const headers = new Headers(); if (init?.headers) { const source = new Headers(init.headers); // Preserve individual set-cookie values for (const cookie of source.getSetCookie()) { headers.append('set-cookie', cookie); } for (const [key, value] of source.entries()) { if (key !== 'set-cookie') headers.set(key, value); } } ``` ### Workaround Disable `cookieCache` so only `session_token` is set (no competing `session_data` to overwrite it): ```ts session: { cookieCache: { enabled: false, }, }, ```
Author
Owner

@bytaesu commented on GitHub (Feb 25, 2026):

@ajxbit Thanks! I'll check 👀

<!-- gh-comment-id:3962760422 --> @bytaesu commented on GitHub (Feb 25, 2026): @ajxbit Thanks! I'll check 👀
Author
Owner

@Ernestas-Zidokas commented on GitHub (Mar 31, 2026):

I have the similar issue with impersonateUser, it looks like only the last cookie is being set.

EDIT:
The issue was Node version
https://github.com/TanStack/router/issues/5464

<!-- gh-comment-id:4165000166 --> @Ernestas-Zidokas commented on GitHub (Mar 31, 2026): I have the similar issue with impersonateUser, it looks like only the last cookie is being set. EDIT: The issue was Node version https://github.com/TanStack/router/issues/5464
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10983