[GH-ISSUE #6164] Password reset link builds with backend port instead of frontend #19062

Closed
opened 2026-04-15 17:50:46 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @mszulfiqar on GitHub (Nov 21, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/6164

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Setup Better Auth on an Express backend running on http://localhost:3000
  2. . In Next.js frontend (running on http://localhost:3001`), call:
    await authClient.requestPasswordReset({
      email: "user@example.com",
      redirectTo: "http://localhost:3001/reset-password"
    });`
    

3.The email arrives with a link like:
http://localhost:3000/api/auth/reset-password/?callbackURL=%2Freset-password
4.Clicking that link doesn’t lead to the frontend reset page.

Current vs. Expected behavior

I expect the reset link in the email to be:
http://localhost:3001/reset-password?token=

What version of Better Auth are you using?

1.3.34

System info

(backend)
npx @better-auth/cli info --json
{
  "system": {
    "platform": "win32",        
    "arch": "x64",
    "version": "Windows 10 Pro",
    "release": "10.0.19045",
    "cpuCount": 4,
    "cpuModel": "Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz",
    "totalMemory": "7.90 GB",
    "freeMemory": "1.04 GB"
  },
  "node": {
    "version": "v22.15.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.5.2"
  },
  "frameworks": [
    {
      "name": "express",
      "version": "^5.1.0"
    }
  ],
  "databases": [
    {
      "name": "@prisma/client",
      "version": "^6.19.0"
    }
  ],
  "betterAuth": {
    "version": "^1.3.34",
    "config": {
      "baseURL": "http://localhost:3001", 
      "trustedOrigins": [
        "http://localhost:3001"
      ],
      "session": {
        "cookieCache": {
          "enabled": true,
          "maxAge": 300
        },
        "expiresIn": 604800,
        "updateAge": 86400
      },
      "emailAndPassword": {
        "enabled": true
      }
    }
  }
}

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

Client

Auth config (if applicable)

(backend:express-js port:3000)
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "./prisma.js"
import { sendEmailFunction } from "./nodemailer.js";
import { emailTemplate } from "../utils/emailTemplate.js";

export const auth = betterAuth({
    baseURL: "http://localhost:3001" ,
    trustedOrigins: ["http://localhost:3001"],
    session: {
        cookieCache: {
            enabled: true,
            maxAge: 5 * 60
        },
        expiresIn: 60 * 60 * 24 * 7,
        updateAge: 60 * 60 * 24
    },
    emailAndPassword: {
        enabled: true,
        sendResetPassword: async ({ user, url, token },request) => {
            console.log(url)
            await sendEmailFunction({
                to:user.email,
                subject:"Reset the password!",
                html:emailTemplate(url)
            })
        }
    },
    database: prismaAdapter(prisma, {
        provider: "postgresql",
    }),
});

(frontend nextjs port:3001)
import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient({
   baseURL: "http://localhost:3000"
});

export const { signIn, signUp, signOut, useSession } = authClient;

Additional context

No response

Originally created by @mszulfiqar on GitHub (Nov 21, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/6164 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Setup Better Auth on an Express backend running on `http://localhost:3000` 2. `. In Next.js frontend (running on `http://localhost:3001`), call: ```ts await authClient.requestPasswordReset({ email: "user@example.com", redirectTo: "http://localhost:3001/reset-password" });` 3.The email arrives with a link like: http://localhost:3000/api/auth/reset-password/<token>?callbackURL=%2Freset-password 4.Clicking that link doesn’t lead to the frontend reset page. ### Current vs. Expected behavior I expect the reset link in the email to be: http://localhost:3001/reset-password?token=<token> ### What version of Better Auth are you using? 1.3.34 ### System info ```bash (backend) npx @better-auth/cli info --json { "system": { "platform": "win32", "arch": "x64", "version": "Windows 10 Pro", "release": "10.0.19045", "cpuCount": 4, "cpuModel": "Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz", "totalMemory": "7.90 GB", "freeMemory": "1.04 GB" }, "node": { "version": "v22.15.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.5.2" }, "frameworks": [ { "name": "express", "version": "^5.1.0" } ], "databases": [ { "name": "@prisma/client", "version": "^6.19.0" } ], "betterAuth": { "version": "^1.3.34", "config": { "baseURL": "http://localhost:3001", "trustedOrigins": [ "http://localhost:3001" ], "session": { "cookieCache": { "enabled": true, "maxAge": 300 }, "expiresIn": 604800, "updateAge": 86400 }, "emailAndPassword": { "enabled": true } } } } ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript (backend:express-js port:3000) import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { prisma } from "./prisma.js" import { sendEmailFunction } from "./nodemailer.js"; import { emailTemplate } from "../utils/emailTemplate.js"; export const auth = betterAuth({ baseURL: "http://localhost:3001" , trustedOrigins: ["http://localhost:3001"], session: { cookieCache: { enabled: true, maxAge: 5 * 60 }, expiresIn: 60 * 60 * 24 * 7, updateAge: 60 * 60 * 24 }, emailAndPassword: { enabled: true, sendResetPassword: async ({ user, url, token },request) => { console.log(url) await sendEmailFunction({ to:user.email, subject:"Reset the password!", html:emailTemplate(url) }) } }, database: prismaAdapter(prisma, { provider: "postgresql", }), }); (frontend nextjs port:3001) import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ baseURL: "http://localhost:3000" }); export const { signIn, signUp, signOut, useSession } = authClient; ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-15 17:50:46 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Nov 21, 2025):

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

Better Auth always generates password reset links using the backend’s host and port, not the frontend’s. The redirectTo parameter only controls where users are redirected after clicking the backend link—it doesn’t change the link itself. There’s no built-in config to make the reset link point directly to your frontend.

To get the reset link you want (e.g., http://localhost:3001/reset-password?token=<token>), customize your sendResetPassword callback. Instead of using the default url parameter, build your own link using the token:

sendResetPassword: async ({ user, token }, request) => {
  const frontendUrl = `http://localhost:3001/reset-password?token=${token}`;
  await sendEmailFunction({
    to: user.email,
    subject: "Reset the password!",
    html: emailTemplate(frontendUrl)
  });
}

This way, users will be sent directly to your frontend reset page with the token as a query parameter. The backend will still validate the token when the frontend submits it. This approach is widely recommended in the community and confirmed by the codebase and docs (source, docs).

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:3563609785 --> @dosubot[bot] commented on GitHub (Nov 21, 2025): <!-- Greeting --> Hi @mszulfiqar! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> Better Auth always generates password reset links using the backend’s host and port, not the frontend’s. The `redirectTo` parameter only controls where users are redirected after clicking the backend link—it doesn’t change the link itself. There’s no built-in config to make the reset link point directly to your frontend. To get the reset link you want (e.g., `http://localhost:3001/reset-password?token=<token>`), customize your `sendResetPassword` callback. Instead of using the default `url` parameter, build your own link using the token: ```ts sendResetPassword: async ({ user, token }, request) => { const frontendUrl = `http://localhost:3001/reset-password?token=${token}`; await sendEmailFunction({ to: user.email, subject: "Reset the password!", html: emailTemplate(frontendUrl) }); } ``` This way, users will be sent directly to your frontend reset page with the token as a query parameter. The backend will still validate the token when the frontend submits it. This approach is widely recommended in the community and confirmed by the codebase and docs ([source](https://github.com/better-auth/better-auth/blob/fc80f8ab43ad30d3474e593cdf8dd231b82595fc/packages/better-auth/src/api/routes/reset-password.ts#L125-L327), [docs](https://github.com/better-auth/better-auth/blob/fc80f8ab43ad30d3474e593cdf8dd231b82595fc/docs/content/docs/authentication/email-password.mdx#L208-L254)). <!-- 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/fc50bac5-e6ed-4775-8da0-8c6112ae9508?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/fc50bac5-e6ed-4775-8da0-8c6112ae9508?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/fc50bac5-e6ed-4775-8da0-8c6112ae9508?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/fc50bac5-e6ed-4775-8da0-8c6112ae9508?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/fc50bac5-e6ed-4775-8da0-8c6112ae9508?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/fc50bac5-e6ed-4775-8da0-8c6112ae9508?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/fc50bac5-e6ed-4775-8da0-8c6112ae9508?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/6164)
Author
Owner

@mszulfiqar commented on GitHub (Nov 21, 2025):

it worked! Thanks

<!-- gh-comment-id:3563795767 --> @mszulfiqar commented on GitHub (Nov 21, 2025): it worked! Thanks
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19062