[GH-ISSUE #4100] cookies are not working in case of backend on remote server that is being used for a hosted frontend, and the local frontend #18458

Closed
opened 2026-04-15 16:54:52 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @lord007tn on GitHub (Aug 20, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4100

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Just host better-auth in api.example.com
and a frontend in example.com
Until this, we are good, no problem
But if I want to work on the localhost and connect to the remote api, cookies don't work, because it's a must to define the domain. I tried removing the domain, and the cookies returned with baseUrl as the default cookie domain, which is not the case, and broke production also

Current vs. Expected behavior

My expectation is that we use the trustedOrigins; if a domain is available there, there is no need to have a hard-coded cookie domain
and this way, we can connect from the local to the remote server and actually do the job

i already talked about that in https://github.com/better-auth/better-auth/issues/3874#issuecomment-3198999324

Can we please focus on solving this, because not all people use NextJS as full-stack framework, and in normal real-world products, we tend to separate frontend and backend for multiple purposes

What version of Better Auth are you using?

1.3.7

System info

System:
    OS: Windows 11 10.0.26100
    CPU: (32) x64 AMD Ryzen 9 7950X 16-Core Processor
    Memory: 3.84 GB / 31.16 GB
  Browsers:
    Edge: Chromium (139.0.3405.102)

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

Package

Auth config (if applicable)

import { prisma } from '@repo/database';
import { resend } from '@repo/email';
import { EmailOtpTemplate, PasswordResetTokenTemplate } from '@repo/email/templates';
import { SystemRoles } from '@repo/shared/constants';
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { nextCookies } from 'better-auth/next-js';
import { admin, emailOTP, openAPI } from 'better-auth/plugins';
import { TRUSTED_ORIGINS } from '../constants';
import { keys } from '../keys';

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: 'postgresql',
  }),
  emailAndPassword: {
    enabled: true,
    autoSignIn: true,
    disableSignUp: keys().DISABLE_SIGNUP,
    resetPasswordTokenExpiresIn: 10 * 60, // 10 minutes
    sendResetPassword: async ({ user, token }) => {
      await resend.emails.send({
        from: keys().RESEND_FROM,
        to: [user.email],
        subject: 'Reset your Rize.gg password',
        react: PasswordResetTokenTemplate({
          username: user.name || user.email.split('@')[0] || 'User',
          resetUrl: `${keys().NEXT_PUBLIC_APP_URL}/reset-password?token=${token}`,
          expiresInMinutes: 10, // Token expires in 10 minutes
        }),
      });
    },
  },
  emailVerification: {
    sendOnSignUp: true,
  },
  advanced: {
    database: {
      generateId: false,
    },
    crossSubDomainCookies: {
      enabled: true,
      domain: keys().NEXT_PUBLIC_COOKIE_DOMAIN,
    },
    useSecureCookies: process.env.NODE_ENV === 'production',
    defaultCookieAttributes: {
      sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
    },
  },
  baseURL: keys().NEXT_PUBLIC_API_URL,
  secret: keys().BETTER_AUTH_SECRET,
  basePath: '/api/app/auth',
  trustedOrigins: TRUSTED_ORIGINS,
  user: {
    fields: {
      name: 'fullName',
    },
  },
  account: {
    accountLinking: {
      enabled: true,
      trustedProviders: ['google', 'twitch', 'discord'],
    },
  },
  databaseHooks: {
    user: {
      create: {
        after: async (user) => {
          // Create empty profile when user is created
          await prisma.profile.upsert({
            where: { userId: user.id },
            update: {},
            create: {
              userId: user.id,
              // @ts-expect-error better-auth doesn't provide fullName
              firstName: user.fullName?.split(' ')[0] || '',
              // @ts-expect-error better-auth doesn't provide fullName
              lastName: user.fullName?.split(' ').slice(1).join(' ') || '',
              avatarUrl: user.image,
            },
          });
        },
      },
    },
  },
  socialProviders: {
    google: {
      clientId: keys().GOOGLE_CLIENT_ID,
      clientSecret: keys().GOOGLE_CLIENT_SECRET,
    },
    twitch: {
      clientId: keys().TWITCH_CLIENT_ID,
      clientSecret: keys().TWITCH_CLIENT_SECRET,
    },
    discord: {
      clientId: keys().DISCORD_CLIENT_ID,
      clientSecret: keys().DISCORD_CLIENT_SECRET,
    },
  },
  plugins: [
    admin({
      defaultRole: SystemRoles.USER,
      adminRoles: [SystemRoles.ADMIN, SystemRoles.SUPER_ADMIN, SystemRoles.SUPPORT],
      schema: {
        session: {
          fields: {
            impersonatedBy: 'impersonatorId',
          },
        },
      },
    }),
    emailOTP({
      overrideDefaultEmailVerification: true,
      expiresIn: 10 * 60, // 10 minutes

      sendVerificationOTP: async ({ email, otp, type }) => {
      
    }),
    openAPI(),
    nextCookies(), // this always the last plugin in array
  ],
}) as ReturnType<typeof betterAuth>;

Additional context

No response

Originally created by @lord007tn on GitHub (Aug 20, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4100 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Just host better-auth in api.example.com and a frontend in example.com Until this, we are good, no problem But if I want to work on the localhost and connect to the remote api, cookies don't work, because it's a must to define the `domain`. I tried removing the domain, and the cookies returned with `baseUrl` as the default cookie domain, which is not the case, and broke production also ### Current vs. Expected behavior My expectation is that we use the `trustedOrigins`; if a domain is available there, there is no need to have a hard-coded cookie domain and this way, we can connect from the local to the remote server and actually do the job i already talked about that in https://github.com/better-auth/better-auth/issues/3874#issuecomment-3198999324 Can we please focus on solving this, because not all people use NextJS as full-stack framework, and in normal real-world products, we tend to separate frontend and backend for multiple purposes ### What version of Better Auth are you using? 1.3.7 ### System info ```bash System: OS: Windows 11 10.0.26100 CPU: (32) x64 AMD Ryzen 9 7950X 16-Core Processor Memory: 3.84 GB / 31.16 GB Browsers: Edge: Chromium (139.0.3405.102) ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript import { prisma } from '@repo/database'; import { resend } from '@repo/email'; import { EmailOtpTemplate, PasswordResetTokenTemplate } from '@repo/email/templates'; import { SystemRoles } from '@repo/shared/constants'; import { betterAuth } from 'better-auth'; import { prismaAdapter } from 'better-auth/adapters/prisma'; import { nextCookies } from 'better-auth/next-js'; import { admin, emailOTP, openAPI } from 'better-auth/plugins'; import { TRUSTED_ORIGINS } from '../constants'; import { keys } from '../keys'; export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: 'postgresql', }), emailAndPassword: { enabled: true, autoSignIn: true, disableSignUp: keys().DISABLE_SIGNUP, resetPasswordTokenExpiresIn: 10 * 60, // 10 minutes sendResetPassword: async ({ user, token }) => { await resend.emails.send({ from: keys().RESEND_FROM, to: [user.email], subject: 'Reset your Rize.gg password', react: PasswordResetTokenTemplate({ username: user.name || user.email.split('@')[0] || 'User', resetUrl: `${keys().NEXT_PUBLIC_APP_URL}/reset-password?token=${token}`, expiresInMinutes: 10, // Token expires in 10 minutes }), }); }, }, emailVerification: { sendOnSignUp: true, }, advanced: { database: { generateId: false, }, crossSubDomainCookies: { enabled: true, domain: keys().NEXT_PUBLIC_COOKIE_DOMAIN, }, useSecureCookies: process.env.NODE_ENV === 'production', defaultCookieAttributes: { sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax', }, }, baseURL: keys().NEXT_PUBLIC_API_URL, secret: keys().BETTER_AUTH_SECRET, basePath: '/api/app/auth', trustedOrigins: TRUSTED_ORIGINS, user: { fields: { name: 'fullName', }, }, account: { accountLinking: { enabled: true, trustedProviders: ['google', 'twitch', 'discord'], }, }, databaseHooks: { user: { create: { after: async (user) => { // Create empty profile when user is created await prisma.profile.upsert({ where: { userId: user.id }, update: {}, create: { userId: user.id, // @ts-expect-error better-auth doesn't provide fullName firstName: user.fullName?.split(' ')[0] || '', // @ts-expect-error better-auth doesn't provide fullName lastName: user.fullName?.split(' ').slice(1).join(' ') || '', avatarUrl: user.image, }, }); }, }, }, }, socialProviders: { google: { clientId: keys().GOOGLE_CLIENT_ID, clientSecret: keys().GOOGLE_CLIENT_SECRET, }, twitch: { clientId: keys().TWITCH_CLIENT_ID, clientSecret: keys().TWITCH_CLIENT_SECRET, }, discord: { clientId: keys().DISCORD_CLIENT_ID, clientSecret: keys().DISCORD_CLIENT_SECRET, }, }, plugins: [ admin({ defaultRole: SystemRoles.USER, adminRoles: [SystemRoles.ADMIN, SystemRoles.SUPER_ADMIN, SystemRoles.SUPPORT], schema: { session: { fields: { impersonatedBy: 'impersonatorId', }, }, }, }), emailOTP({ overrideDefaultEmailVerification: true, expiresIn: 10 * 60, // 10 minutes sendVerificationOTP: async ({ email, otp, type }) => { }), openAPI(), nextCookies(), // this always the last plugin in array ], }) as ReturnType<typeof betterAuth>; ``` ### Additional context _No response_
GiteaMirror added the lockedenhancement labels 2026-04-15 16:54:52 -05:00
Author
Owner

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

Better Auth does not currently support dynamic cookie domain assignment based on trustedOrigins or the incoming request's origin. The cookie domain is set statically at startup using the advanced.crossSubDomainCookies.domain config or, if omitted, defaults to the hostname of baseURL. This means you can't have a single config that works for both production (custom domain) and local development (localhost) without changing the domain setting between environments. Cookies set for a remote domain (like .example.com) are not accessible from localhost due to browser restrictions, and vice versa see docs.

The recommended approach is to use environment variables to switch the cookie domain and related attributes (like sameSite and secure) depending on your environment. For example, in production, set the domain to .example.com and in development, either disable crossSubDomainCookies or set the domain to localhost (if your backend is running locally). Make sure to also set sameSite: 'none' and secure: true for cross-origin cookies, and use credentials: 'include' in your frontend requests see issue #3874.

There is no built-in way to leverage trustedOrigins for cookie domain assignment; it is only used for CORS and security checks, not for cookie handling. If you want a seamless local-to-remote workflow, consider using a local proxy or a custom domain mapped to localhost for development, so your frontend and backend share a common parent domain.

Let me know if you want example environment configs or more details on proxy setups!

To reply, just mention @dosu.


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

<!-- gh-comment-id:3204077629 --> @dosubot[bot] commented on GitHub (Aug 20, 2025): <!-- Answer --> Better Auth does not currently support dynamic cookie domain assignment based on `trustedOrigins` or the incoming request's origin. The cookie domain is set statically at startup using the `advanced.crossSubDomainCookies.domain` config or, if omitted, defaults to the hostname of `baseURL`. This means you can't have a single config that works for both production (custom domain) and local development (localhost) without changing the domain setting between environments. Cookies set for a remote domain (like `.example.com`) are not accessible from `localhost` due to browser restrictions, and vice versa [see docs](https://better-auth.com/docs/concepts/cookies). The recommended approach is to use environment variables to switch the cookie domain and related attributes (like `sameSite` and `secure`) depending on your environment. For example, in production, set the domain to `.example.com` and in development, either disable `crossSubDomainCookies` or set the domain to `localhost` (if your backend is running locally). Make sure to also set `sameSite: 'none'` and `secure: true` for cross-origin cookies, and use `credentials: 'include'` in your frontend requests [see issue #3874](https://github.com/better-auth/better-auth/issues/3874). There is no built-in way to leverage `trustedOrigins` for cookie domain assignment; it is only used for CORS and security checks, not for cookie handling. If you want a seamless local-to-remote workflow, consider using a local proxy or a custom domain mapped to localhost for development, so your frontend and backend share a common parent domain. Let me know if you want example environment configs or more details on proxy setups! <!-- 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/2e1dc659-9bd7-40e1-8bc2-d13bd47ba70d?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/2e1dc659-9bd7-40e1-8bc2-d13bd47ba70d?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/2e1dc659-9bd7-40e1-8bc2-d13bd47ba70d?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/2e1dc659-9bd7-40e1-8bc2-d13bd47ba70d?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/2e1dc659-9bd7-40e1-8bc2-d13bd47ba70d?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/2e1dc659-9bd7-40e1-8bc2-d13bd47ba70d?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/2e1dc659-9bd7-40e1-8bc2-d13bd47ba70d?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/4100)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18458