Google Redirect not working on Production #2495

Closed
opened 2026-03-13 09:59:11 -05:00 by GiteaMirror · 10 comments
Owner

Originally created by @irorochad on GitHub (Dec 9, 2025).

Originally assigned to: @Paola3stefania on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I'm using Better-auth with nextjs and expressjs. I'm using cookies to authenticate my users, but i'm having a domain (crosssite) issue on production, causing my redirects not to work as intended.

Current vs. Expected behavior

Current Behavior: Due to diffeernt domain names frontend, and backend uses different domains), the app doesn't redirect to the fronted after Google 0auth authentication on production.

Redirect url on google console is: https://parental-domain.koyeb.app/api/auth/callback/google (backend).
my login/sign up google auth is configured as follows

 await authClient.signIn.social({
                provider: "google",
                callbackURL: `${process.env.NEXT_PUBLIC_URL}/dashboard`, //this env var is set to frontend url (i can confirm on both prod, and local
   })

The expected behvaiour is it's suppose to redirect to the frontend url, and set the cookies there for users to be able to login to the account!

What version of Better Auth are you using?

^1.3.13

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.5.0: Tue Apr 22 19:48:46 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T8103",
    "release": "24.5.0",
    "cpuCount": 8,
    "cpuModel": "Apple M1",
    "totalMemory": "8.00 GB",
    "freeMemory": "0.14 GB"
  },
  "node": {
    "version": "v23.7.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": [
    {
      "name": "express",
      "version": "^4.18.2"
    }
  ],
  "databases": [
    {
      "name": "@prisma/client",
      "version": "^5.22.0"
    },
    {
      "name": "@neondatabase/serverless",
      "version": "^1.0.2"
    }
  ],
  "betterAuth": {
    "version": "^1.3.13",
    "config": {
      "emailAndPassword": {
        "enabled": true,
        "autoSignIn": true
      },
      "session": {
        "expiresIn": 604800,
        "updateAge": 3600,
        "cookieCache": {
          "enabled": true,
          "maxAge": 300
        }
      },
      "advanced": {
        "cookiePrefix": "better-auth",
        "crossSubDomainCookies": {
          "enabled": false
        },
        "defaultCookieAttributes": {
          "sameSite": "lax",
          "secure": false,
          "httpOnly": true,
          "path": "/",
          "partitioned": false
        }
      },
      "baseURL": "http://localhost:8080",
      "secret": "[REDACTED]",
      "trustedOrigins": [
        "http://localhost:3000",
        "https://site.vercel.app",
        "*.vercel.app"
      ],
      "socialProviders": {
        "google": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]"
        }
      },
      "plugins": [
        {
          "name": "skip-state-mismatch",
          "config": {
            "id": "skip-state-mismatch"
          }
        },
        {
          "name": "custom-session",
          "config": {
            "id": "custom-session",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$Infer": {
              "Session": {}
            }
          }
        }
      ]
    }
  }
}

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { customSession } from "better-auth/plugins";
import type { BetterAuthPlugin } from "better-auth";
import { PrismaClient } from "@prisma/client";
import { createUserWorkspace, getUserWorkspace, setupUserTrial } from "./services/userService.js";
import { getTrialStatus } from "./utils/trial.js";
import { getUserSubscription } from "./services/subscriptionService.js";

const prisma = new PrismaClient();

/**
 * Skip OAuth State Mismatch Plugin
 * 
 * This plugin disables the OAuth state cookie check, which is necessary for cross-domain setups
 * where the OAuth state cookie can't be properly read due to domain differences.
 * The OAuth state cookie gets set by the backend but can't be read back properly.
 * 
 * @see https://github.com/better-auth/better-auth/issues/4969#issuecomment-3547344061
 * 
 */
export function skipStateMismatch(): BetterAuthPlugin {
  return {
    id: 'skip-state-mismatch',
    init(ctx) {
      return {
        context: {
          ...ctx,
          oauthConfig: {
            skipStateCookieCheck: true,
            ...ctx?.oauthConfig,
          },
        },
      }
    },
  }
}

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),

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

  /**
   * Session Configuration
   * - Sessions expire after 7 days of inactivity
   * - Session data is refreshed every hour to keep workspace/subscription info current
   * - Cookie settings configured for cross-domain (production)
   */
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60, // 1 hour - refresh session data hourly
    cookieCache: {
      enabled: true,
      maxAge: 60 * 5, // 5 minutes
    },
  },

  /**
   * Advanced Cookie Configuration for Cross-Domain Auth
   * CRITICAL for production where frontend and backend are on different domains
   * 
   * When frontend (web.vercel.app) and backend (koyeb.app) are on different domains:
   * - sameSite must be "none" to allow cross-site cookies
   * - secure must be true (HTTPS only) when sameSite is "none"
   * - partitioned must be true (new browser standard for foreign/third-party cookies)
   * - domain should NOT be set (let browser handle it)
   */
  advanced: {
    cookiePrefix: "better-auth",
    crossSubDomainCookies: {
      enabled: false, // We're on completely different domains, not subdomains
    },
    defaultCookieAttributes: {
      sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
      secure: process.env.NODE_ENV === 'production' ? true : false,
      httpOnly: true,
      path: '/',
      partitioned: process.env.NODE_ENV === 'production' ? true : false, // CRITICAL for cross-domain
      // Do NOT set domain - let it default to the current domain
      // Setting domain can cause issues with cross-domain cookies
    },
  },

  baseURL: process.env.BETTER_AUTH_URL,

  secret: process.env.BETTER_AUTH_SECRET,
  trustedOrigins: [
    // Development
    "http://localhost:3000",
    "https://test.vercel.app",
    "*.vercel.app", // all other vercel previews
    // "https://custom-domain.com"
  ],
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    },
  },
  plugins: [
    skipStateMismatch(), // Fix OAuth state cookie cross-domain issue
    customSession(async ({ user, session }) => {
      // some custom user codes here, not affecting cookies at all!
    })
  ]
});

Additional context

PLEASE HELP ME DEBUG THIS! It's working well for localhost but not on production, and it's simply because of the domain being different .

Originally created by @irorochad on GitHub (Dec 9, 2025). Originally assigned to: @Paola3stefania on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce I'm using Better-auth with nextjs and expressjs. I'm using cookies to authenticate my users, but i'm having a domain (crosssite) issue on production, causing my redirects not to work as intended. ### Current vs. Expected behavior Current Behavior: Due to diffeernt domain names frontend, and backend uses different domains), the app doesn't redirect to the fronted after Google 0auth authentication on production. Redirect url on google console is: https://parental-domain.koyeb.app/api/auth/callback/google (backend). my login/sign up google auth is configured as follows ``` await authClient.signIn.social({ provider: "google", callbackURL: `${process.env.NEXT_PUBLIC_URL}/dashboard`, //this env var is set to frontend url (i can confirm on both prod, and local }) ``` The expected behvaiour is it's suppose to redirect to the frontend url, and set the cookies there for users to be able to login to the account! ### What version of Better Auth are you using? ^1.3.13 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.5.0: Tue Apr 22 19:48:46 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T8103", "release": "24.5.0", "cpuCount": 8, "cpuModel": "Apple M1", "totalMemory": "8.00 GB", "freeMemory": "0.14 GB" }, "node": { "version": "v23.7.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": [ { "name": "express", "version": "^4.18.2" } ], "databases": [ { "name": "@prisma/client", "version": "^5.22.0" }, { "name": "@neondatabase/serverless", "version": "^1.0.2" } ], "betterAuth": { "version": "^1.3.13", "config": { "emailAndPassword": { "enabled": true, "autoSignIn": true }, "session": { "expiresIn": 604800, "updateAge": 3600, "cookieCache": { "enabled": true, "maxAge": 300 } }, "advanced": { "cookiePrefix": "better-auth", "crossSubDomainCookies": { "enabled": false }, "defaultCookieAttributes": { "sameSite": "lax", "secure": false, "httpOnly": true, "path": "/", "partitioned": false } }, "baseURL": "http://localhost:8080", "secret": "[REDACTED]", "trustedOrigins": [ "http://localhost:3000", "https://site.vercel.app", "*.vercel.app" ], "socialProviders": { "google": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]" } }, "plugins": [ { "name": "skip-state-mismatch", "config": { "id": "skip-state-mismatch" } }, { "name": "custom-session", "config": { "id": "custom-session", "hooks": { "after": [ {} ] }, "endpoints": {}, "$Infer": { "Session": {} } } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { customSession } from "better-auth/plugins"; import type { BetterAuthPlugin } from "better-auth"; import { PrismaClient } from "@prisma/client"; import { createUserWorkspace, getUserWorkspace, setupUserTrial } from "./services/userService.js"; import { getTrialStatus } from "./utils/trial.js"; import { getUserSubscription } from "./services/subscriptionService.js"; const prisma = new PrismaClient(); /** * Skip OAuth State Mismatch Plugin * * This plugin disables the OAuth state cookie check, which is necessary for cross-domain setups * where the OAuth state cookie can't be properly read due to domain differences. * The OAuth state cookie gets set by the backend but can't be read back properly. * * @see https://github.com/better-auth/better-auth/issues/4969#issuecomment-3547344061 * */ export function skipStateMismatch(): BetterAuthPlugin { return { id: 'skip-state-mismatch', init(ctx) { return { context: { ...ctx, oauthConfig: { skipStateCookieCheck: true, ...ctx?.oauthConfig, }, }, } }, } } export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: "postgresql", }), emailAndPassword: { enabled: true, autoSignIn: true, }, /** * Session Configuration * - Sessions expire after 7 days of inactivity * - Session data is refreshed every hour to keep workspace/subscription info current * - Cookie settings configured for cross-domain (production) */ session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60, // 1 hour - refresh session data hourly cookieCache: { enabled: true, maxAge: 60 * 5, // 5 minutes }, }, /** * Advanced Cookie Configuration for Cross-Domain Auth * CRITICAL for production where frontend and backend are on different domains * * When frontend (web.vercel.app) and backend (koyeb.app) are on different domains: * - sameSite must be "none" to allow cross-site cookies * - secure must be true (HTTPS only) when sameSite is "none" * - partitioned must be true (new browser standard for foreign/third-party cookies) * - domain should NOT be set (let browser handle it) */ advanced: { cookiePrefix: "better-auth", crossSubDomainCookies: { enabled: false, // We're on completely different domains, not subdomains }, defaultCookieAttributes: { sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax', secure: process.env.NODE_ENV === 'production' ? true : false, httpOnly: true, path: '/', partitioned: process.env.NODE_ENV === 'production' ? true : false, // CRITICAL for cross-domain // Do NOT set domain - let it default to the current domain // Setting domain can cause issues with cross-domain cookies }, }, baseURL: process.env.BETTER_AUTH_URL, secret: process.env.BETTER_AUTH_SECRET, trustedOrigins: [ // Development "http://localhost:3000", "https://test.vercel.app", "*.vercel.app", // all other vercel previews // "https://custom-domain.com" ], socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }, }, plugins: [ skipStateMismatch(), // Fix OAuth state cookie cross-domain issue customSession(async ({ user, session }) => { // some custom user codes here, not affecting cookies at all! }) ] }); ``` ### Additional context PLEASE HELP ME DEBUG THIS! It's working well for localhost but not on production, and it's simply because of the domain being different .
GiteaMirror added the express label 2026-03-13 09:59:11 -05:00
Author
Owner

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

The root cause is browser security: when your frontend and backend are on completely different domains, browsers block cookies set by the backend during OAuth redirects, even if the Set-Cookie header looks correct. This is why it works on localhost (where browser rules are relaxed) but fails in production with different domains ref.

Here’s how to debug and fix:

  1. Check Set-Cookie headers: In your browser’s network tab, after the OAuth callback, confirm if the Set-Cookie header is present and what attributes it has. If the cookie isn’t stored, it’s likely due to domain or attribute issues ref.
  2. Remove partitioned: true: Setting partitioned: true can cause cookies to be siloed and not shared across domains. Set partitioned: false in your config ref.
  3. Keep sameSite: 'none' and secure: true: These are required for cross-site cookies. Do not set the domain attribute if your frontend and backend are on completely different domains ref.
  4. CORS and credentials: Your backend must send Access-Control-Allow-Origin matching your frontend and Access-Control-Allow-Credentials: true. All frontend requests (including the OAuth callback) must use credentials: 'include' ref.
  5. Browser limitations: Some browsers (Safari, Brave) block third-party cookies by default. Test in Chrome/Edge and avoid public suffixes (like *.vercel.app or *.koyeb.app) for production ref.
  6. Best practice: For reliable cross-domain auth, use subdomains of the same root domain (e.g., app.example.com and api.example.com) and enable crossSubDomainCookies with domain: '.example.com' ref.
  7. Proxy/CDN interference: If you use Vercel, Cloudflare, or similar, make sure they aren’t stripping or modifying Set-Cookie headers ref.
  8. Delaying redirect: Sometimes, the redirect after OAuth happens before the cookie is set. Try delaying the client-side redirect or using a success hook to ensure the cookie is stored before navigation ref.

If you can’t use the same root domain for both frontend and backend, browser security will prevent cookies from being shared, and there’s no reliable workaround—this is a browser limitation, not a Better Auth bug ref.

If you need more help, share a screenshot of your network tab showing the Set-Cookie header and the redirect chain after login.

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

@dosubot[bot] commented on GitHub (Dec 9, 2025): <!-- Answer --> The root cause is browser security: when your frontend and backend are on completely different domains, browsers block cookies set by the backend during OAuth redirects, even if the Set-Cookie header looks correct. This is why it works on localhost (where browser rules are relaxed) but fails in production with different domains [ref](https://github.com/better-auth/better-auth/discussions/3262#discussioncomment-13665380). Here’s how to debug and fix: 1. **Check Set-Cookie headers:** In your browser’s network tab, after the OAuth callback, confirm if the Set-Cookie header is present and what attributes it has. If the cookie isn’t stored, it’s likely due to domain or attribute issues [ref](https://github.com/better-auth/better-auth/issues/4038#issuecomment-3472540279). 2. **Remove `partitioned: true`:** Setting `partitioned: true` can cause cookies to be siloed and not shared across domains. Set `partitioned: false` in your config [ref](https://github.com/better-auth/better-auth/discussions/3262#discussioncomment-13665380). 3. **Keep `sameSite: 'none'` and `secure: true`:** These are required for cross-site cookies. Do not set the `domain` attribute if your frontend and backend are on completely different domains [ref](https://github.com/better-auth/better-auth/issues/3938). 4. **CORS and credentials:** Your backend must send `Access-Control-Allow-Origin` matching your frontend and `Access-Control-Allow-Credentials: true`. All frontend requests (including the OAuth callback) must use `credentials: 'include'` [ref](https://github.com/better-auth/better-auth/blob/7ff3fe01d87261364a53c66a26879908a22b1ff1/docs/content/docs/integrations/hono.mdx#L26-L186). 5. **Browser limitations:** Some browsers (Safari, Brave) block third-party cookies by default. Test in Chrome/Edge and avoid public suffixes (like `*.vercel.app` or `*.koyeb.app`) for production [ref](https://github.com/better-auth/better-auth/issues/4038). 6. **Best practice:** For reliable cross-domain auth, use subdomains of the same root domain (e.g., `app.example.com` and `api.example.com`) and enable `crossSubDomainCookies` with `domain: '.example.com'` [ref](https://github.com/better-auth/better-auth/discussions/3262). 7. **Proxy/CDN interference:** If you use Vercel, Cloudflare, or similar, make sure they aren’t stripping or modifying Set-Cookie headers [ref](https://github.com/better-auth/better-auth/issues/3938). 8. **Delaying redirect:** Sometimes, the redirect after OAuth happens before the cookie is set. Try delaying the client-side redirect or using a success hook to ensure the cookie is stored before navigation [ref](https://github.com/better-auth/better-auth/issues/4038). If you can’t use the same root domain for both frontend and backend, browser security will prevent cookies from being shared, and there’s no reliable workaround—this is a browser limitation, not a Better Auth bug [ref](https://github.com/better-auth/better-auth/issues/4038). If you need more help, share a screenshot of your network tab showing the Set-Cookie header and the redirect chain after login. <!-- 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/4c724e24-163d-444d-a18c-d6c39fdb39aa?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/4c724e24-163d-444d-a18c-d6c39fdb39aa?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/4c724e24-163d-444d-a18c-d6c39fdb39aa?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/4c724e24-163d-444d-a18c-d6c39fdb39aa?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/4c724e24-163d-444d-a18c-d6c39fdb39aa?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/4c724e24-163d-444d-a18c-d6c39fdb39aa?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/4c724e24-163d-444d-a18c-d6c39fdb39aa?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=The%20root%20cause%20is%20browser%20security%3A%20when%20your%20frontend%20and%20backend%20are%20on%20completely%20different%20domains%2C%20browsers%20block%20cookies%20set%20by%20the%20backend%20during%20OAuth%20redirects%2C%20even%20if%20the%20Set-Cookie%20header%20looks%20correct.%20This%20is%20why%20it%20works%20on%20localhost%20%28where%20browser%20rules%20are%20relaxed%29%20but%20fails%20in%20production%20with%20different%20domains%C2%A0%5Bref%5D%28https%3A//github.com/better-auth/better-auth/discussions/3262%23discussioncomment-13665380%29.%0A%0AHere%E2%80%99s%20how%20to%20debug%20and%20fix%3A%0A%0A1.%20%2A%2ACheck%20Set-Cookie%20headers%3A%2A%2A%20In%20your%20browser%E2%80%99s%20network%20tab%2C%20after%20the%20OAuth%20callback%2C%20confirm%20if%20the%20Set-Cookie%20header%20is%20present%20and%20what%20attributes%20it%20has.%20If%20the%20cookie%20isn%E2%80%99t%20stored%2C%20it%E2%80%99s%20likely%20due%20to%20domain%20or%20attribute%20issues%C2%A0%5Bref%5D%28https%3A//github.com/better-auth/better-auth/issues/4038%23issuecomment-3472540279%29.%0A2.%20%2A%2ARemove%20%60partitioned%3A%20true%60%3A%2A%2A%20Setting%20%60partitioned%3A%20true%60%20can%20cause%20cookies%20to%20be%20siloed%20and%20not%20shared%20across%20domains.%20Set%20%60partitioned%3A%20false%60%20in%20your%20config%C2%A0%5Bref%5D%28https%3A//github.com/better-auth/better-auth/discussions/3262%23discussioncomment-13665380%29.%0A3.%20%2A%2AKeep%20%60sameSite%3A%20%27none%27%60%20and%20%60secure%3A%20true%60%3A%2A%2A%20These%20are%20required%20for%20cross-site%20cookies.%20Do%20not%20set%20the%20%60domain%60%20attribute%20if%20your%20frontend%20and%20backend%20are%20on%20completely%20different%20domains%C2%A0%5Bref%5D%28https%3A//github.com/better-auth/better-auth/issues/3938%29.%0A4.%20%2A%2ACORS%20and%20credentials%3A%2A%2A%20Your%20backend%20must%20send%20%60Access-Control-Allow-Origin%60%20matching%20your%20frontend%20and%20%60Access-Control-Allow-Credentials%3A%20true%60.%20All%20frontend%20requests%20%28including%20the%20OAuth%20callback%29%20must%20use%20%60credentials%3A%20%27include%27%60%C2%A0%5Bref%5D%28https%3A//github.com/better-auth/better-auth/blob/7ff3fe01d87261364a53c66a26879908a22b1ff1/docs/content/docs/integrations/hono.mdx%23L26-L186%29.%0A5.%20%2A%2ABrowser%20limitations%3A%2A%2A%20Some%20browsers%20%28Safari%2C%20Brave%29%20block%20third-party%20cookies%20by%20default.%20Test%20in%20Chrome/Edge%20and%20avoid%20public%20suffixes%20%28like%20%60%2A.vercel.app%60%20or%20%60%2A.koyeb.app%60%29%20for%20production%C2%A0%5Bref%5D%28https%3A//github.com/better-auth/better-auth/issues/4038%29.%0A6.%20%2A%2ABest%20practice%3A%2A%2A%20For%20reliable%20cross-domain%20auth%2C%20use%20subdomains%20of%20the%20same%20root%20domain%20%28e.g.%2C%20%60app.example.com%60%20and%20%60api.example.com%60%29%20and%20enable%20%60crossSubDomainCookies%60%20with%20%60domain%3A%20%27.example.com%27%60%C2%A0%5Bref%5D%28https%3A//github.com/better-auth/better-auth/discussions/3262%29.%0A7.%20%2A%2AProxy/CDN%20interference%3A%2A%2A%20If%20you%20use%20Vercel%2C%20Cloudflare%2C%20or%20similar%2C%20make%20sure%20they%20aren%E2%80%99t%20stripping%20or%20modifying%20Set-Cookie%20headers%C2%A0%5Bref%5D%28https%3A//github.com/better-auth/better-auth/issues/3938%29.%0A8.%20%2A%2ADelaying%20redirect%3A%2A%2A%20Sometimes%2C%20the%20redirect%20after%20OAuth%20happens%20before%20the%20cookie%20is%20set.%20Try%20delaying%20the%20client-side%20redirect%20or%20using%20a%20success%20hook%20to%20ensure%20the%20cookie%20is%20stored%20before%20navigation%C2%A0%5Bref%5D%28https%3A//github.com/better-auth/better-auth/issues/4038%29.%0A%0AIf%20you%20can%E2%80%99t%20use%20the%20same%20root%20domain%20for%20both%20frontend%20and%20backend%2C%20browser%20security%20will%20prevent%20cookies%20from%20being%20shared%2C%20and%20there%E2%80%99s%20no%20reliable%20workaround%E2%80%94this%20is%20a%20browser%20limitation%2C%20not%20a%20Better%20Auth%20bug%C2%A0%5Bref%5D%28https%3A//github.com/better-auth/better-auth/issues/4038%29.%0A%0AIf%20you%20need%20more%20help%2C%20share%20a%20screenshot%20of%20your%20network%20tab%20showing%20the%20Set-Cookie%20header%20and%20the%20redirect%20chain%20after%20login.)&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/6645)
Author
Owner

@irorochad commented on GitHub (Dec 9, 2025):

@dosu, you're right to mention that it's not a bug, but a browser limitation. I still haven't been able to figure it out yet. my cookies are being set when i login with normal email and password, but using google 0auth redirects me to the backend url still. eei, i'm tired lol.

@irorochad commented on GitHub (Dec 9, 2025): @dosu, you're right to mention that it's not a bug, but a browser limitation. I still haven't been able to figure it out yet. my cookies are being set when i login with normal email and password, but using google 0auth redirects me to the backend url still. eei, i'm tired lol.
Author
Owner

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

You're super close—this is a classic cross-domain OAuth pain point. Here’s what’s happening and how to fix it:

  • With email/password, everything happens on your backend, so cookies are set and read on the same domain—no problem.
  • With Google OAuth, the flow is: frontend → backend → Google → backend → frontend. The backend sets the cookie, then redirects to the frontend. But if your frontend and backend are on totally different domains (not subdomains), browsers won’t send the cookie to the frontend, and sometimes the redirect lands you back on the backend if the callback URL isn’t absolute or trusted.

Checklist to get Google OAuth redirecting to your frontend:

  1. Make sure you’re passing a full, absolute URL as callbackURL in your authClient.signIn.social call (e.g., https://your-frontend-domain/dashboard). If you use a relative path like /dashboard, you’ll get redirected to the backend’s domain, not the frontend’s (source).
  2. Your backend’s trustedOrigins must include the exact frontend origin (e.g., https://your-frontend-domain) (source).
  3. Your Google Cloud Console redirect URI should be your backend’s callback endpoint (e.g., https://your-backend-domain/api/auth/callback/google) (source).
  4. For cross-domain cookies, set sameSite: 'none', secure: true, and set partitioned: false (partitioned cookies are siloed and won’t work for this flow) (source).
  5. If you’re using public suffixes like vercel.app or koyeb.app, some browsers will block cookies entirely. For production, use custom domains or subdomains of the same root domain if possible (source).
  6. Make sure CORS allows credentials and the frontend always sends requests with credentials: 'include' (source).

Debug tip: Open your browser’s network tab, watch the OAuth callback and final redirect, and check the Set-Cookie headers and which domain they’re set on. If you see the cookie set on the backend but not available on the frontend, it’s a browser limitation.

If you’re stuck with different domains, there’s no reliable way to share cookies due to browser security. The only robust fix is to use subdomains of the same root domain (e.g., app.example.com and api.example.com) and enable cross-subdomain cookies.

Let me know if you want a concrete config example for your exact domains!

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

@dosubot[bot] commented on GitHub (Dec 9, 2025): <!-- Answer --> You're super close—this is a classic cross-domain OAuth pain point. Here’s what’s happening and how to fix it: - With email/password, everything happens on your backend, so cookies are set and read on the same domain—no problem. - With Google OAuth, the flow is: frontend → backend → Google → backend → frontend. The backend sets the cookie, then redirects to the frontend. But if your frontend and backend are on totally different domains (not subdomains), browsers won’t send the cookie to the frontend, and sometimes the redirect lands you back on the backend if the callback URL isn’t absolute or trusted. **Checklist to get Google OAuth redirecting to your frontend:** 1. Make sure you’re passing a full, absolute URL as `callbackURL` in your `authClient.signIn.social` call (e.g., `https://your-frontend-domain/dashboard`). If you use a relative path like `/dashboard`, you’ll get redirected to the backend’s domain, not the frontend’s ([source](https://github.com/better-auth/better-auth/issues/3289)). 2. Your backend’s `trustedOrigins` must include the exact frontend origin (e.g., `https://your-frontend-domain`) ([source](https://github.com/better-auth/better-auth/issues/3289)). 3. Your Google Cloud Console redirect URI should be your backend’s callback endpoint (e.g., `https://your-backend-domain/api/auth/callback/google`) ([source](https://github.com/better-auth/better-auth/discussions/5253)). 4. For cross-domain cookies, set `sameSite: 'none'`, `secure: true`, and **set `partitioned: false`** (partitioned cookies are siloed and won’t work for this flow) ([source](https://github.com/better-auth/better-auth/discussions/3262)). 5. If you’re using public suffixes like `vercel.app` or `koyeb.app`, some browsers will block cookies entirely. For production, use custom domains or subdomains of the same root domain if possible ([source](https://github.com/better-auth/better-auth/discussions/3262)). 6. Make sure CORS allows credentials and the frontend always sends requests with `credentials: 'include'` ([source](https://github.com/better-auth/better-auth/issues/3938)). **Debug tip:** Open your browser’s network tab, watch the OAuth callback and final redirect, and check the `Set-Cookie` headers and which domain they’re set on. If you see the cookie set on the backend but not available on the frontend, it’s a browser limitation. If you’re stuck with different domains, there’s no reliable way to share cookies due to browser security. The only robust fix is to use subdomains of the same root domain (e.g., `app.example.com` and `api.example.com`) and enable cross-subdomain cookies. Let me know if you want a concrete config example for your exact domains! <!-- 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/fccdf565-ba3f-4fa5-9cd3-986aa088f365?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/fccdf565-ba3f-4fa5-9cd3-986aa088f365?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/fccdf565-ba3f-4fa5-9cd3-986aa088f365?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/fccdf565-ba3f-4fa5-9cd3-986aa088f365?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/fccdf565-ba3f-4fa5-9cd3-986aa088f365?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/fccdf565-ba3f-4fa5-9cd3-986aa088f365?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/fccdf565-ba3f-4fa5-9cd3-986aa088f365?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=You%27re%20super%20close%E2%80%94this%20is%20a%20classic%20cross-domain%20OAuth%20pain%20point.%20Here%E2%80%99s%20what%E2%80%99s%20happening%20and%20how%20to%20fix%20it%3A%0A%0A-%20With%20email/password%2C%20everything%20happens%20on%20your%20backend%2C%20so%20cookies%20are%20set%20and%20read%20on%20the%20same%20domain%E2%80%94no%20problem.%0A-%20With%20Google%20OAuth%2C%20the%20flow%20is%3A%20frontend%20%E2%86%92%20backend%20%E2%86%92%20Google%20%E2%86%92%20backend%20%E2%86%92%20frontend.%20The%20backend%20sets%20the%20cookie%2C%20then%20redirects%20to%20the%20frontend.%20But%20if%20your%20frontend%20and%20backend%20are%20on%20totally%20different%20domains%20%28not%20subdomains%29%2C%20browsers%20won%E2%80%99t%20send%20the%20cookie%20to%20the%20frontend%2C%20and%20sometimes%20the%20redirect%20lands%20you%20back%20on%20the%20backend%20if%20the%20callback%20URL%20isn%E2%80%99t%20absolute%20or%20trusted.%0A%0A%2A%2AChecklist%20to%20get%20Google%20OAuth%20redirecting%20to%20your%20frontend%3A%2A%2A%0A%0A1.%20Make%20sure%20you%E2%80%99re%20passing%20a%20full%2C%20absolute%20URL%20as%20%60callbackURL%60%20in%20your%20%60authClient.signIn.social%60%20call%20%28e.g.%2C%20%60https%3A//your-frontend-domain/dashboard%60%29.%20If%20you%20use%20a%20relative%20path%20like%20%60/dashboard%60%2C%20you%E2%80%99ll%20get%20redirected%20to%20the%20backend%E2%80%99s%20domain%2C%20not%20the%20frontend%E2%80%99s%C2%A0%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/3289%29%29.%0A2.%20Your%20backend%E2%80%99s%20%60trustedOrigins%60%20must%20include%20the%20exact%20frontend%20origin%20%28e.g.%2C%20%60https%3A//your-frontend-domain%60%29%C2%A0%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/3289%29%29.%0A3.%20Your%20Google%20Cloud%20Console%20redirect%20URI%20should%20be%20your%20backend%E2%80%99s%20callback%20endpoint%20%28e.g.%2C%20%60https%3A//your-backend-domain/api/auth/callback/google%60%29%C2%A0%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/discussions/5253%29%29.%0A4.%20For%20cross-domain%20cookies%2C%20set%20%60sameSite%3A%20%27none%27%60%2C%20%60secure%3A%20true%60%2C%20and%20%2A%2Aset%20%60partitioned%3A%20false%60%2A%2A%20%28partitioned%20cookies%20are%20siloed%20and%20won%E2%80%99t%20work%20for%20this%20flow%29%C2%A0%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/discussions/3262%29%29.%0A5.%20If%20you%E2%80%99re%20using%20public%20suffixes%20like%20%60vercel.app%60%20or%20%60koyeb.app%60%2C%20some%20browsers%20will%20block%20cookies%20entirely.%20For%20production%2C%20use%20custom%20domains%20or%20subdomains%20of%20the%20same%20root%20domain%20if%20possible%C2%A0%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/discussions/3262%29%29.%0A6.%20Make%20sure%20CORS%20allows%20credentials%20and%20the%20frontend%20always%20sends%20requests%20with%20%60credentials%3A%20%27include%27%60%C2%A0%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/3938%29%29.%0A%0A%2A%2ADebug%20tip%3A%2A%2A%20Open%20your%20browser%E2%80%99s%20network%20tab%2C%20watch%20the%20OAuth%20callback%20and%20final%20redirect%2C%20and%20check%20the%20%60Set-Cookie%60%20headers%20and%20which%20domain%20they%E2%80%99re%20set%20on.%20If%20you%20see%20the%20cookie%20set%20on%20the%20backend%20but%20not%20available%20on%20the%20frontend%2C%20it%E2%80%99s%20a%20browser%20limitation.%0A%0AIf%20you%E2%80%99re%20stuck%20with%20different%20domains%2C%20there%E2%80%99s%20no%20reliable%20way%20to%20share%20cookies%20due%20to%20browser%20security.%20The%20only%20robust%20fix%20is%20to%20use%20subdomains%20of%20the%20same%20root%20domain%20%28e.g.%2C%20%60app.example.com%60%20and%20%60api.example.com%60%29%20and%20enable%20cross-subdomain%20cookies.%0A%0ALet%20me%20know%20if%20you%20want%20a%20concrete%20config%20example%20for%20your%20exact%20domains%21)&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/6645)
Author
Owner

@ayinde-xyz commented on GitHub (Dec 24, 2025):

Currently facing similar issues

@ayinde-xyz commented on GitHub (Dec 24, 2025): Currently facing similar issues
Author
Owner

@rahulkr-dev commented on GitHub (Jan 10, 2026):

@ayinde-xyz
Has your issue been resolved? I’m facing the same problem.

I want to allow (or set) all cross-domains that are listed in my Trusted Origins. I’m setting the origin dynamically, but it doesn’t seem to work as expected.

Any update or guidance would be really helpful.

@rahulkr-dev commented on GitHub (Jan 10, 2026): @ayinde-xyz Has your issue been resolved? I’m facing the same problem. I want to allow (or set) all cross-domains that are listed in my Trusted Origins. I’m setting the origin dynamically, but it doesn’t seem to work as expected. Any update or guidance would be really helpful.
Author
Owner

@irorochad commented on GitHub (Jan 10, 2026):

The solution is simply to use a domain with the same root origin. on dev
mode, local host appears to be the same origin for frontend and backend. In
production, you can try to use the same domain for both the frontend, and
backend (api).

On Sat, Jan 10, 2026, 5:27 AM Rahul kumar @.***> wrote:

rahulkr-dev left a comment (better-auth/better-auth#6645)
https://github.com/better-auth/better-auth/issues/6645#issuecomment-3731809103

@ayinde-xyz https://github.com/ayinde-xyz
Has your issue been resolved? I’m facing the same problem.

I want to allow (or set) all cross-domains that are listed in my Trusted
Origins. I’m setting the origin dynamically, but it doesn’t seem to work as
expected.

Any update or guidance would be really helpful.


Reply to this email directly, view it on GitHub
https://github.com/better-auth/better-auth/issues/6645#issuecomment-3731809103,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ANQ3AUZCOMZXUFMKTAL7GED4GB5URAVCNFSM6AAAAACORDGS6CVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTOMZRHAYDSMJQGM
.
You are receiving this because you authored the thread.Message ID:
@.***>

@irorochad commented on GitHub (Jan 10, 2026): The solution is simply to use a domain with the same root origin. on dev mode, local host appears to be the same origin for frontend and backend. In production, you can try to use the same domain for both the frontend, and backend (api). On Sat, Jan 10, 2026, 5:27 AM Rahul kumar ***@***.***> wrote: > *rahulkr-dev* left a comment (better-auth/better-auth#6645) > <https://github.com/better-auth/better-auth/issues/6645#issuecomment-3731809103> > > @ayinde-xyz <https://github.com/ayinde-xyz> > Has your issue been resolved? I’m facing the same problem. > > I want to allow (or set) all cross-domains that are listed in my Trusted > Origins. I’m setting the origin dynamically, but it doesn’t seem to work as > expected. > > Any update or guidance would be really helpful. > > — > Reply to this email directly, view it on GitHub > <https://github.com/better-auth/better-auth/issues/6645#issuecomment-3731809103>, > or unsubscribe > <https://github.com/notifications/unsubscribe-auth/ANQ3AUZCOMZXUFMKTAL7GED4GB5URAVCNFSM6AAAAACORDGS6CVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTOMZRHAYDSMJQGM> > . > You are receiving this because you authored the thread.Message ID: > ***@***.***> >
Author
Owner

@Paola3stefania commented on GitHub (Jan 11, 2026):

Hi @irorochad,

Thanks for the detailed config, its quite useful to have as many as info as possible :) I can see what's happening here.

You're facing two separate but related problems:

  1. OAuth State Cookie Issue - Your skipStateMismatch() plugin addresses this by setting skipStateCookieCheck: true

  2. Session Cookie Cross-Domain Issue - This is the actual blocker. Even after OAuth completes successfully on your backend (koyeb.app), the session cookie is set on koyeb.app. When redirecting to your frontend (vercel.app), browsers cannot read cookies from a different domain. These are completely separate domains, not subdomains...

Your cookie configuration is correct (sameSite: 'none', secure: true, partitioned: true), but this doesn't help when frontend and backend are on entirely different domains - cookies simply can't be shared across koyeb.appvercel.app

Why It Doesn't Work

Your skipStateMismatch() plugin only affects OAuth state validation.

The flow looks like this:

  1. User clicks "Sign in with Google" on vercel.app
  2. Redirected to Google, authenticates
  3. Google redirects to koyeb.app/api/auth/callback/google
  4. Backend creates session, sets session cookie on koyeb.app
  5. Backend redirects to vercel.app/dashboard
  6. Frontend tries to read session cookie - but it doesn't exist on vercel.app!

The oAuthProxy plugin was designed specifically for this scenario. It encrypts session cookies and passes them via URL parameters to bypass cross-domain restrictions:

import { betterAuth } from "better-auth";
import { oAuthProxy } from "better-auth/plugins";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { customSession } from "better-auth/plugins";

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),

  plugins: [
    oAuthProxy({
      productionURL: "https://parental-domain.koyeb.app", // Your backend URL
      // currentURL will be inferred, but you can set it explicitly:
      // currentURL: "https://site.vercel.app",
    }),
    customSession(async ({ user, session }) => {
      // your custom session logic
    }),
  ],

  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
      // IMPORTANT: Must set redirectURI to your production backend
      redirectURI: "https://parental-domain.koyeb.app/api/auth/callback/google",
    },
  },

  // ... rest of your config (session, advanced, etc.)
});

The oAuthProxy plugin fixes steps 4 to 6 of the flow by encrypting cookies into URL parameters and setting them on the correct domain
You can remove your custom skipStateMismatch() plugin - the oAuthProxy plugin handles this internally.

Solution 2: Use the Built-in Option Instead of Custom Plugin

If you still need skipStateCookieCheck for other reasons, use the built-in option instead of a custom plugin:

export const auth = betterAuth({
  account: {
    skipStateCookieCheck: true, // Built-in option
  },
  // ...
});

However, note that this alone won't fix your redirect issue - you still need the oAuthProxy plugin for cross-domain cookie proper handling

Solution 3: Use Same Domain (best for long term)

For production, the most reliable approach is to put both frontend and backend on the same root domain:

  • Frontend: app.yourdomain.com
  • Backend: api.yourdomain.com

Then enable cross-subdomain cookies:

advanced: {
  crossSubDomainCookies: {
    enabled: true,
    domain: ".yourdomain.com"
  }
}

This avoids all the cross-domain cookie complexity.

Additional Notes

  • The warning about .vercel.app in our docs applies here - Vercel preview domains are treated as public suffixes, making cookie sharing even more restricted
  • Make sure your BETTER_AUTH_SECRET is the same across all environments
  • The oAuthProxy plugin has security implications (as noted in docs) but is designed exactly for dev/staging/cross-domain production scenarios like yours

Let me know if you have any questions!


@Paola3stefania commented on GitHub (Jan 11, 2026): Hi @irorochad, Thanks for the detailed config, its quite useful to have as many as info as possible :) I can see what's happening here. You're facing **two separate but related problems**: 1. **OAuth State Cookie Issue** - Your `skipStateMismatch()` plugin addresses this by setting `skipStateCookieCheck: true` 2. **Session Cookie Cross-Domain Issue** - This is the **actual blocker**. Even after OAuth completes successfully on your backend (`koyeb.app`), the session cookie is set on `koyeb.app`. When redirecting to your frontend (`vercel.app`), browsers **cannot read cookies from a different domain**. These are completely separate domains, not subdomains... Your cookie configuration is correct (`sameSite: 'none'`, `secure: true`, `partitioned: true`), but this doesn't help when frontend and backend are on **entirely different domains** - cookies simply can't be shared across `koyeb.app` ↔ `vercel.app` ### Why It Doesn't Work Your `skipStateMismatch()` plugin only affects OAuth state validation. The flow looks like this: 1. User clicks "Sign in with Google" on `vercel.app` 2. Redirected to Google, authenticates 3. Google redirects to `koyeb.app/api/auth/callback/google` 4. Backend creates session, sets session cookie **on `koyeb.app`** 5. _Backend redirects to `vercel.app/dashboard`_ 6. Frontend tries to read session cookie - **but it doesn't exist on `vercel.app`!** ### Solution 1: Use the OAuth Proxy Plugin (Recommended for your setup) The `oAuthProxy` plugin was designed specifically for this scenario. It encrypts session cookies and passes them via URL parameters to bypass cross-domain restrictions: ```ts import { betterAuth } from "better-auth"; import { oAuthProxy } from "better-auth/plugins"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { customSession } from "better-auth/plugins"; export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: "postgresql", }), plugins: [ oAuthProxy({ productionURL: "https://parental-domain.koyeb.app", // Your backend URL // currentURL will be inferred, but you can set it explicitly: // currentURL: "https://site.vercel.app", }), customSession(async ({ user, session }) => { // your custom session logic }), ], socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, // IMPORTANT: Must set redirectURI to your production backend redirectURI: "https://parental-domain.koyeb.app/api/auth/callback/google", }, }, // ... rest of your config (session, advanced, etc.) }); ``` The `oAuthProxy` plugin fixes steps **4 to 6** of the flow by encrypting cookies into URL parameters and setting them on the correct domain You can **remove your custom `skipStateMismatch()` plugin** - the `oAuthProxy` plugin handles this internally. ### Solution 2: Use the Built-in Option Instead of Custom Plugin If you still need `skipStateCookieCheck` for other reasons, use the built-in option instead of a custom plugin: ```ts export const auth = betterAuth({ account: { skipStateCookieCheck: true, // Built-in option }, // ... }); ``` However, note that **this alone won't fix your redirect issue** - you still need the `oAuthProxy` plugin for cross-domain cookie proper handling ### Solution 3: Use Same Domain (best for long term) For production, the most reliable approach is to put both frontend and backend on the same root domain: - Frontend: `app.yourdomain.com` - Backend: `api.yourdomain.com` Then enable cross-subdomain cookies: ```ts advanced: { crossSubDomainCookies: { enabled: true, domain: ".yourdomain.com" } } ``` This avoids all the cross-domain cookie complexity. ### Additional Notes - The warning about `.vercel.app` in our docs applies here - Vercel preview domains are treated as [public suffixes](https://publicsuffix.org/), making cookie sharing even more restricted - Make sure your `BETTER_AUTH_SECRET` is the same across all environments - The `oAuthProxy` plugin has security implications (as noted in docs) but is designed exactly for dev/staging/cross-domain production scenarios like yours Let me know if you have any questions! ---
Author
Owner

@irorochad commented on GitHub (Jan 11, 2026):

Hi @Paola3stefania , thank you so much for your detailed explanation about the problem. I must agree that the workaround for this issue is quite high. Just as your solution 3 says, for long-term, it's best to use the same domain. I've already decided to use saame domain. Thank you once again for the detailed explanation.

@irorochad commented on GitHub (Jan 11, 2026): Hi @Paola3stefania , thank you so much for your detailed explanation about the problem. I must agree that the workaround for this issue is quite high. Just as your solution 3 says, for long-term, it's best to use the same domain. I've already decided to use saame domain. Thank you once again for the detailed explanation.
Author
Owner

@Paola3stefania commented on GitHub (Jan 13, 2026):

Happy to help. I will close this issue now. If anything please feel free to reach out 😊

@Paola3stefania commented on GitHub (Jan 13, 2026): Happy to help. I will close this issue now. If anything please feel free to reach out 😊
Author
Owner

@pranavgoel29 commented on GitHub (Feb 16, 2026):

Hi @irorochad,

Thanks for the detailed config, its quite useful to have as many as info as possible :) I can see what's happening here.

You're facing two separate but related problems:

1. **OAuth State Cookie Issue** - Your `skipStateMismatch()` plugin addresses this by setting `skipStateCookieCheck: true`

2. **Session Cookie Cross-Domain Issue** - This is the **actual blocker**. Even after OAuth completes successfully on your backend (`koyeb.app`), the session cookie is set on `koyeb.app`. When redirecting to your frontend (`vercel.app`), browsers **cannot read cookies from a different domain**. These are completely separate domains, not subdomains...

Your cookie configuration is correct (sameSite: 'none', secure: true, partitioned: true), but this doesn't help when frontend and backend are on entirely different domains - cookies simply can't be shared across koyeb.appvercel.app

Why It Doesn't Work

Your skipStateMismatch() plugin only affects OAuth state validation.

The flow looks like this:

1. User clicks "Sign in with Google" on `vercel.app`

2. Redirected to Google, authenticates

3. Google redirects to `koyeb.app/api/auth/callback/google`

4. Backend creates session, sets session cookie **on `koyeb.app`**

5. _Backend redirects to `vercel.app/dashboard`_

6. Frontend tries to read session cookie - **but it doesn't exist on `vercel.app`!**

The oAuthProxy plugin was designed specifically for this scenario. It encrypts session cookies and passes them via URL parameters to bypass cross-domain restrictions:

import { betterAuth } from "better-auth";
import { oAuthProxy } from "better-auth/plugins";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { customSession } from "better-auth/plugins";

export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "postgresql",
}),

plugins: [
oAuthProxy({
productionURL: "https://parental-domain.koyeb.app", // Your backend URL
// currentURL will be inferred, but you can set it explicitly:
// currentURL: "https://site.vercel.app",
}),
customSession(async ({ user, session }) => {
// your custom session logic
}),
],

socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
// IMPORTANT: Must set redirectURI to your production backend
redirectURI: "https://parental-domain.koyeb.app/api/auth/callback/google",
},
},

// ... rest of your config (session, advanced, etc.)
});

The oAuthProxy plugin fixes steps 4 to 6 of the flow by encrypting cookies into URL parameters and setting them on the correct domain You can remove your custom skipStateMismatch() plugin - the oAuthProxy plugin handles this internally.

Solution 2: Use the Built-in Option Instead of Custom Plugin

If you still need skipStateCookieCheck for other reasons, use the built-in option instead of a custom plugin:

export const auth = betterAuth({
account: {
skipStateCookieCheck: true, // Built-in option
},
// ...
});

However, note that this alone won't fix your redirect issue - you still need the oAuthProxy plugin for cross-domain cookie proper handling

Solution 3: Use Same Domain (best for long term)

For production, the most reliable approach is to put both frontend and backend on the same root domain:

* Frontend: `app.yourdomain.com`

* Backend: `api.yourdomain.com`

Then enable cross-subdomain cookies:

advanced: {
crossSubDomainCookies: {
enabled: true,
domain: ".yourdomain.com"
}
}

This avoids all the cross-domain cookie complexity.

Additional Notes

* The warning about `.vercel.app` in our docs applies here - Vercel preview domains are treated as [public suffixes](https://publicsuffix.org/), making cookie sharing even more restricted

* Make sure your `BETTER_AUTH_SECRET` is the same across all environments

* The `oAuthProxy` plugin has security implications (as noted in docs) but is designed exactly for dev/staging/cross-domain production scenarios like yours

Let me know if you have any questions!

Would you mind sharing the oAuthProxy plugin documentation here? It would be a great resource. Also, I was wondering if this setup would work if I’m running a proxy locally to a deployed BetterAuth instance?

@pranavgoel29 commented on GitHub (Feb 16, 2026): > Hi [@irorochad](https://github.com/irorochad), > > Thanks for the detailed config, its quite useful to have as many as info as possible :) I can see what's happening here. > > You're facing **two separate but related problems**: > > 1. **OAuth State Cookie Issue** - Your `skipStateMismatch()` plugin addresses this by setting `skipStateCookieCheck: true` > > 2. **Session Cookie Cross-Domain Issue** - This is the **actual blocker**. Even after OAuth completes successfully on your backend (`koyeb.app`), the session cookie is set on `koyeb.app`. When redirecting to your frontend (`vercel.app`), browsers **cannot read cookies from a different domain**. These are completely separate domains, not subdomains... > > > Your cookie configuration is correct (`sameSite: 'none'`, `secure: true`, `partitioned: true`), but this doesn't help when frontend and backend are on **entirely different domains** - cookies simply can't be shared across `koyeb.app` ↔ `vercel.app` > ### Why It Doesn't Work > > Your `skipStateMismatch()` plugin only affects OAuth state validation. > > The flow looks like this: > > 1. User clicks "Sign in with Google" on `vercel.app` > > 2. Redirected to Google, authenticates > > 3. Google redirects to `koyeb.app/api/auth/callback/google` > > 4. Backend creates session, sets session cookie **on `koyeb.app`** > > 5. _Backend redirects to `vercel.app/dashboard`_ > > 6. Frontend tries to read session cookie - **but it doesn't exist on `vercel.app`!** > > > ### Solution 1: Use the OAuth Proxy Plugin (Recommended for your setup) > > The `oAuthProxy` plugin was designed specifically for this scenario. It encrypts session cookies and passes them via URL parameters to bypass cross-domain restrictions: > > import { betterAuth } from "better-auth"; > import { oAuthProxy } from "better-auth/plugins"; > import { prismaAdapter } from "better-auth/adapters/prisma"; > import { customSession } from "better-auth/plugins"; > > export const auth = betterAuth({ > database: prismaAdapter(prisma, { > provider: "postgresql", > }), > > plugins: [ > oAuthProxy({ > productionURL: "https://parental-domain.koyeb.app", // Your backend URL > // currentURL will be inferred, but you can set it explicitly: > // currentURL: "https://site.vercel.app", > }), > customSession(async ({ user, session }) => { > // your custom session logic > }), > ], > > socialProviders: { > google: { > clientId: process.env.GOOGLE_CLIENT_ID as string, > clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, > // IMPORTANT: Must set redirectURI to your production backend > redirectURI: "https://parental-domain.koyeb.app/api/auth/callback/google", > }, > }, > > // ... rest of your config (session, advanced, etc.) > }); > > The `oAuthProxy` plugin fixes steps **4 to 6** of the flow by encrypting cookies into URL parameters and setting them on the correct domain You can **remove your custom `skipStateMismatch()` plugin** - the `oAuthProxy` plugin handles this internally. > ### Solution 2: Use the Built-in Option Instead of Custom Plugin > > If you still need `skipStateCookieCheck` for other reasons, use the built-in option instead of a custom plugin: > > export const auth = betterAuth({ > account: { > skipStateCookieCheck: true, // Built-in option > }, > // ... > }); > > However, note that **this alone won't fix your redirect issue** - you still need the `oAuthProxy` plugin for cross-domain cookie proper handling > ### Solution 3: Use Same Domain (best for long term) > > For production, the most reliable approach is to put both frontend and backend on the same root domain: > > * Frontend: `app.yourdomain.com` > > * Backend: `api.yourdomain.com` > > > Then enable cross-subdomain cookies: > > advanced: { > crossSubDomainCookies: { > enabled: true, > domain: ".yourdomain.com" > } > } > > This avoids all the cross-domain cookie complexity. > ### Additional Notes > > * The warning about `.vercel.app` in our docs applies here - Vercel preview domains are treated as [public suffixes](https://publicsuffix.org/), making cookie sharing even more restricted > > * Make sure your `BETTER_AUTH_SECRET` is the same across all environments > > * The `oAuthProxy` plugin has security implications (as noted in docs) but is designed exactly for dev/staging/cross-domain production scenarios like yours > > > Let me know if you have any questions! Would you mind sharing the `oAuthProxy` plugin documentation here? It would be a great resource. Also, I was wondering if this setup would work if I’m running a proxy locally to a deployed BetterAuth instance?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2495