Issue: 307 Temporary Redirect After Sign-In in Production #711

Closed
opened 2026-03-13 08:01:21 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @zachuri on GitHub (Feb 19, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Deploy the Next.js app to Vercel.
  2. Deploy the Hono API to Cloudflare.
  3. Attempt to sign in.
  4. Observe the redirect response and notice the 307 Temporary Redirect status.

Current vs. Expected behavior

After signing in, the user should be redirected from the sign-in page to the dashboard. Locally, when running Next.js on localhost:3000 and Hono API on localhost:8787, the redirection works as expected.

However, when deploying the Next.js app on Vercel and the Hono API on Cloudflare, the redirect results in a 307 Temporary Redirect instead of navigating correctly to the dashboard.

Expected Behavior

After a successful sign-in, the user should be redirected directly to the dashboard.

Actual Behavior

In the production environment (Vercel + Cloudflare), the redirect results in a 307 Temporary Redirect, causing unexpected behavior.

Image

Image

What version of Better Auth are you using?

1.1.14

Provide environment information

- OS: macOS Sequoia Version 15.3.1
- Browser: Brave

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

Other

Auth config (if applicable)

export function createBetterAuthConfig(dbInstance: any, c: Context<AppContext>) {
  // Use the context to access environment variables
  const configuredProviders = enabledProviders.reduce<
    Record<string, { clientId: string; clientSecret: string }>
  >((acc, provider) => {
    const id = env(c)[`${provider.toUpperCase()}_CLIENT_ID`] as string
    const secret = env(c)[`${provider.toUpperCase()}_CLIENT_SECRET`] as string
    if (id && id.length > 0 && secret && secret.length > 0) {
      acc[provider] = { clientId: id, clientSecret: secret }
    }
    return acc
  }, {})

  return {
    baseURL: env(c).API_DOMAIN, // API URL
    trustedOrigins: [env(c).API_DOMAIN, env(c).WEB_DOMAIN], // Needed for cross domain cookies
    database: drizzleAdapter(dbInstance, {
      provider: 'pg',
    }),
    emailAndPassword: {
      enabled: true,
    },
    socialProviders: configuredProviders,
    advanced: {
      crossSubDomainCookies: {
        enabled: true, // Enables cross-domain cookies
      },
      defaultCookieAttributes: {
        sameSite: 'none', // Required for cross-domain cookies
        secure: true, // Ensures cookies are only sent over HTTPS
      },
    },
    rateLimit: {
      window: 10, // time window in seconds
      max: 100, // max requests in the window
    },
  }
}

export function betterAuthCorsMiddleware(c: Context<AppContext>) {
  return cors({
    origin: [env(c).WEB_DOMAIN || 'http://localhost:3000'], // Use env var for frontend domain
    allowHeaders: ['Content-Type', 'Authorization'],
    allowMethods: ['POST', 'GET', 'OPTIONS'],
    exposeHeaders: ['Content-Length'],
    maxAge: 600,
    credentials: true, // Required for cookies to work cross-origin
  })
}

Additional context

This is running a separate frontend (Next JS) and backend (Hono/Cloudflare) which cross domain cookies need to be work.
Works as expected in a local environment (localhost).
Potential issue with how Cloudflare or Vercel handles redirects.
Might be related to how responses are cached or how fetch() handles redirects.

Originally created by @zachuri on GitHub (Feb 19, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Deploy the Next.js app to Vercel. 2. Deploy the Hono API to Cloudflare. 3. Attempt to sign in. 4. Observe the redirect response and notice the 307 Temporary Redirect status. ### Current vs. Expected behavior After signing in, the user should be redirected from the sign-in page to the dashboard. Locally, when running Next.js on localhost:3000 and Hono API on localhost:8787, the redirection works as expected. However, when deploying the Next.js app on Vercel and the Hono API on Cloudflare, the redirect results in a 307 Temporary Redirect instead of navigating correctly to the dashboard. ## Expected Behavior After a successful sign-in, the user should be redirected directly to the dashboard. ## Actual Behavior In the production environment (Vercel + Cloudflare), the redirect results in a 307 Temporary Redirect, causing unexpected behavior. ![Image](https://github.com/user-attachments/assets/f67ee43b-11d1-44fb-915e-092d4b414849) ![Image](https://github.com/user-attachments/assets/811ff74f-8bba-4dfd-90bc-4d8f5daca4e4) ### What version of Better Auth are you using? 1.1.14 ### Provide environment information ```bash - OS: macOS Sequoia Version 15.3.1 - Browser: Brave ``` ### Which area(s) are affected? (Select all that apply) Other ### Auth config (if applicable) ```typescript export function createBetterAuthConfig(dbInstance: any, c: Context<AppContext>) { // Use the context to access environment variables const configuredProviders = enabledProviders.reduce< Record<string, { clientId: string; clientSecret: string }> >((acc, provider) => { const id = env(c)[`${provider.toUpperCase()}_CLIENT_ID`] as string const secret = env(c)[`${provider.toUpperCase()}_CLIENT_SECRET`] as string if (id && id.length > 0 && secret && secret.length > 0) { acc[provider] = { clientId: id, clientSecret: secret } } return acc }, {}) return { baseURL: env(c).API_DOMAIN, // API URL trustedOrigins: [env(c).API_DOMAIN, env(c).WEB_DOMAIN], // Needed for cross domain cookies database: drizzleAdapter(dbInstance, { provider: 'pg', }), emailAndPassword: { enabled: true, }, socialProviders: configuredProviders, advanced: { crossSubDomainCookies: { enabled: true, // Enables cross-domain cookies }, defaultCookieAttributes: { sameSite: 'none', // Required for cross-domain cookies secure: true, // Ensures cookies are only sent over HTTPS }, }, rateLimit: { window: 10, // time window in seconds max: 100, // max requests in the window }, } } export function betterAuthCorsMiddleware(c: Context<AppContext>) { return cors({ origin: [env(c).WEB_DOMAIN || 'http://localhost:3000'], // Use env var for frontend domain allowHeaders: ['Content-Type', 'Authorization'], allowMethods: ['POST', 'GET', 'OPTIONS'], exposeHeaders: ['Content-Length'], maxAge: 600, credentials: true, // Required for cookies to work cross-origin }) } ``` ### Additional context This is running a separate frontend (Next JS) and backend (Hono/Cloudflare) which cross domain cookies need to be work. Works as expected in a local environment (localhost). Potential issue with how Cloudflare or Vercel handles redirects. Might be related to how responses are cached or how fetch() handles redirects.
GiteaMirror added the bug label 2026-03-13 08:01:21 -05:00
Author
Owner

@ducheharsh commented on GitHub (May 3, 2025):

hey can u share how did you make it work cross domain cookies on localhost for oauth?

@ducheharsh commented on GitHub (May 3, 2025): hey can u share how did you make it work cross domain cookies on localhost for oauth?
Author
Owner

@ducheharsh commented on GitHub (May 3, 2025):

i tried using the suggested hono/client thing on frontend but it simply does not work, it would be really helpful if you shared the client side setup
this is my setup

Backend

// hono backend
import { CustomDrizzleClient } from "@/db";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { Env } from "@/types/bindings";
import { account, session, user, verification } from "@/db/schema/auth-schema";

// Create a dummy DB client for the CLI to work with
// This will only be used when running the CLI, not in production
const dummyDb = new CustomDrizzleClient({
    url: process.env.DATABASE_URL || "file:./dev.db",
    authToken: process.env.DATABASE_AUTH_TOKEN || ""
});


export const auth = betterAuth({
    database: drizzleAdapter(dummyDb, {
        provider: "sqlite", // or "mysql", "sqlite"
    })
});


// Factory function that creates an auth instance with the provided env
// Use this in your serverless routes
export const createAuth = async (env: Env) => {
    const drizzleClient = new CustomDrizzleClient({
        url: env.DATABASE_URL,
        authToken: env.DATABASE_AUTH_TOKEN
    });

    const db = await drizzleClient.client();

    return betterAuth({
        database: drizzleAdapter(db, {
            provider: "sqlite",
            schema: {
                user,
                session,
                account,
                verification
            } // or "mysql", "sqlite"
        }),

        trustedOrigins: ['http://localhost:3000'],

        emailAndPassword: {
            enabled: true
        },

        socialProviders: {
            google: {
                clientId: process.env.GOOGLE_CLIENT_ID as string,
                clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
            },

        },
        advanced: {
            crossSubDomainCookies: {
                enabled: true, // Enables cross-domain cookies
            },
            defaultCookieAttributes: {
                sameSite: 'none', // Required for cross-domain cookies
                secure: true, // Ensures cookies are only sent over HTTPS
            },
        },
        rateLimit: {
            window: 10, // time window in seconds
            max: 100, // max requests in the window
        },

    });
};

Frontend

import {
    createAuthClient
} from "better-auth/react";

export const authClient = createAuthClient({
    baseURL: "http://localhost:8787",
})

export const {
    signIn,
    signOut,
    signUp,
    useSession
} = authClient;
@ducheharsh commented on GitHub (May 3, 2025): i tried using the suggested hono/client thing on frontend but it simply does not work, it would be really helpful if you shared the client side setup this is my setup ### Backend ```ts // hono backend import { CustomDrizzleClient } from "@/db"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { Env } from "@/types/bindings"; import { account, session, user, verification } from "@/db/schema/auth-schema"; // Create a dummy DB client for the CLI to work with // This will only be used when running the CLI, not in production const dummyDb = new CustomDrizzleClient({ url: process.env.DATABASE_URL || "file:./dev.db", authToken: process.env.DATABASE_AUTH_TOKEN || "" }); export const auth = betterAuth({ database: drizzleAdapter(dummyDb, { provider: "sqlite", // or "mysql", "sqlite" }) }); // Factory function that creates an auth instance with the provided env // Use this in your serverless routes export const createAuth = async (env: Env) => { const drizzleClient = new CustomDrizzleClient({ url: env.DATABASE_URL, authToken: env.DATABASE_AUTH_TOKEN }); const db = await drizzleClient.client(); return betterAuth({ database: drizzleAdapter(db, { provider: "sqlite", schema: { user, session, account, verification } // or "mysql", "sqlite" }), trustedOrigins: ['http://localhost:3000'], emailAndPassword: { enabled: true }, socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }, }, advanced: { crossSubDomainCookies: { enabled: true, // Enables cross-domain cookies }, defaultCookieAttributes: { sameSite: 'none', // Required for cross-domain cookies secure: true, // Ensures cookies are only sent over HTTPS }, }, rateLimit: { window: 10, // time window in seconds max: 100, // max requests in the window }, }); }; ``` ### Frontend ```ts import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ baseURL: "http://localhost:8787", }) export const { signIn, signOut, signUp, useSession } = authClient; ```
Author
Owner

@danger9224 commented on GitHub (Jun 18, 2025):

I'm experiencing the same with a Next.JS API route deployed on Vercel + an Expo app trying to login. Interestingly it's not happening on localhost (running locally)

Other routes (like the ones from our own API) do work, it's only better-auth that dies because of this.

@danger9224 commented on GitHub (Jun 18, 2025): I'm experiencing the same with a Next.JS API route deployed on Vercel + an Expo app trying to login. Interestingly it's not happening on localhost (running locally) Other routes (like the ones from our own API) do work, it's only better-auth that dies because of this.
Author
Owner

@danger9224 commented on GitHub (Jun 23, 2025):

For anyone struggling with a similar issue, here's some of my mistakes. They weren't related to better-auth though, just some rookie mistakes with web/api dev :D

  • First of all, ensure that you're pointing to the right www/www-less variant (eg. if you're using domain.com as your base, and www.domain.com redirects to domain.com, also point your apps to domain.com and not www.domain.com)
  • Your request might get caught up due to badly configured CORS. Unfortunately I'm really no expert on this topic, but this guide explains it nicely enough: https://vercel.com/guides/how-to-enable-cors

For me, for example, setting up a vercel.json file that looks like these has helped resolve my issues!

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "trailingSlash": true, // < this might not be needed in your case - for me it's needed for Posthog reverse proxying
  "rewrites": [
    {
      "source": "/api/:path*",
      "destination": "/api/:path*"
    }
  ]
}
@danger9224 commented on GitHub (Jun 23, 2025): For anyone struggling with a similar issue, here's some of my mistakes. They weren't related to better-auth though, just some rookie mistakes with web/api dev :D - First of all, ensure that you're pointing to the right www/www-less variant (eg. if you're using `domain.com` as your base, and `www.domain.com` redirects to `domain.com`, also point your apps to `domain.com` and not `www.domain.com`) - Your request might get caught up due to badly configured CORS. Unfortunately I'm really no expert on this topic, but this guide explains it nicely enough: https://vercel.com/guides/how-to-enable-cors For me, for example, setting up a `vercel.json` file that looks like these has helped resolve my issues! ``` { "$schema": "https://openapi.vercel.sh/vercel.json", "trailingSlash": true, // < this might not be needed in your case - for me it's needed for Posthog reverse proxying "rewrites": [ { "source": "/api/:path*", "destination": "/api/:path*" } ] } ```
Author
Owner

@zachuri commented on GitHub (Jun 24, 2025):

Hi yall,

Following the BetterAuth documentation on cross-domain cookies, the solution involves conditionally setting cookie attributes based on the environment:

  • sameSite should be:

    • 'none' when running locally
    • 'lax' in production
  • secure is always true (required when sameSite: 'none')

  • domain is conditionally set only in production using the extracted domain from WEB_DOMAIN

defaultCookieAttributes: {
  sameSite: isProduction ? 'lax' : 'none',
  secure: true,
  domain: isProduction ? extractDomain(env(c).WEB_DOMAIN) : undefined,
}

This ensures proper handling of cross-domain cookies in both environments.

Other Notes

  • Adds isProduction check via env(c).env
  • Ensures trusted origins include both API and Web domains
  • Keeps social provider setup and rate limits unchanged
@zachuri commented on GitHub (Jun 24, 2025): Hi yall, Following the [BetterAuth documentation on cross-domain cookies](https://www.better-auth.com/docs/integrations/hono#cross-domain-cookies), the solution involves conditionally setting cookie attributes based on the environment: * **`sameSite`** should be: * `'none'` when running **locally** * `'lax'` in **production** * **`secure`** is always `true` (required when `sameSite: 'none'`) * **`domain`** is conditionally set only in production using the extracted domain from `WEB_DOMAIN` ```ts defaultCookieAttributes: { sameSite: isProduction ? 'lax' : 'none', secure: true, domain: isProduction ? extractDomain(env(c).WEB_DOMAIN) : undefined, } ``` This ensures proper handling of cross-domain cookies in both environments. #### Other Notes * Adds `isProduction` check via `env(c).env` * Ensures trusted origins include both API and Web domains * Keeps social provider setup and rate limits unchanged
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#711