[GH-ISSUE #5936] Could not override default email verification with emailOTP plugin #19012

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

Originally created by @Shridhad on GitHub (Nov 12, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5936

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

The quickest way to reproduce would be to add following test case in email-otp.test.ts#L932

Note that the system default emailVerification is enabled on signup and also emailOTP plugin is configured to overrideDefaultEmailVerification

it("should override default email verification", async () => {
	const sendVerificationEmail = vi.fn();
	const { client } = await getTestInstance({
		emailAndPassword: {
			enabled: true,
		},
		emailVerification: {
			sendOnSignUp: true,
			async sendVerificationEmail(data, request) {
				sendVerificationEmail(data, request);
			},
		},
		plugins: [
			emailOTP({
				overrideDefaultEmailVerification: true,
				async sendVerificationOTP(data, request) {
					//
				},
			}),
		],
	});
	await client.signUp.email(
		{
			email: "test-otp-override@email.com",
			password: "password",
			name: "Test User",
		},
		{
			onSuccess: cookieSetter(headers),
		},
	);
	expect(sendVerificationEmail).not.toHaveBeenCalled();
})

Current vs. Expected behavior

Current:
The emailVerification.sendVerificationEmail is invoked, and two emails are send to new user - both verification link and another one with OTP

Expected:
The emailVerification.sendVerificationEmail should be invoked. Only OTP should be sent to user.

What version of Better Auth are you using?

v1.4.0-beta.20

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:15:09 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6041",
    "release": "24.6.0",
    "cpuCount": 16,
    "cpuModel": "Apple M4 Max",
    "totalMemory": "64.00 GB",
    "freeMemory": "0.15 GB"
  },
  "node": {
    "version": "v22.19.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.8.2"
  },
  "frameworks": [
    {
      "name": "fastify",
      "version": "^5.5.0"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.16.0"
    },
    {
      "name": "kysely",
      "version": "^0.28.8"
    }
  ],
  "betterAuth": {
    "version": "v1.4.0-beta.20",
    "config": null
  }
}

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
  emailVerification: {
		sendOnSignUp: true,
		async sendVerificationEmail(data, request) {
			sendVerificationEmail(data, request);
		},
	},
	plugins: [
		emailOTP({
			overrideDefaultEmailVerification: true,
			async sendVerificationOTP(data, request) {
				//
			},
		}),
	]
});

Additional context

It seems the defu does not override the values in destination object at context/helpers.tx#L33 and treats plugin returned options as defaults

However, when emailVerification.sendVerificationEmail is not defined, everything works fine.

Originally created by @Shridhad on GitHub (Nov 12, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5936 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce The quickest way to reproduce would be to add following test case in [email-otp.test.ts#L932](https://github.com/better-auth/better-auth/blob/31cb70c7e9358466c5faf9fd94ea1764b1721f54/packages/better-auth/src/plugins/email-otp/email-otp.test.ts#L932) Note that the system default `emailVerification` is enabled on signup and also emailOTP plugin is configured to `overrideDefaultEmailVerification` ```ts it("should override default email verification", async () => { const sendVerificationEmail = vi.fn(); const { client } = await getTestInstance({ emailAndPassword: { enabled: true, }, emailVerification: { sendOnSignUp: true, async sendVerificationEmail(data, request) { sendVerificationEmail(data, request); }, }, plugins: [ emailOTP({ overrideDefaultEmailVerification: true, async sendVerificationOTP(data, request) { // }, }), ], }); await client.signUp.email( { email: "test-otp-override@email.com", password: "password", name: "Test User", }, { onSuccess: cookieSetter(headers), }, ); expect(sendVerificationEmail).not.toHaveBeenCalled(); }) ``` ### Current vs. Expected behavior Current: The `emailVerification.sendVerificationEmail` is invoked, and two emails are send to new user - both verification link and another one with OTP Expected: The `emailVerification.sendVerificationEmail` should be invoked. Only OTP should be sent to user. ### What version of Better Auth are you using? v1.4.0-beta.20 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:15:09 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6041", "release": "24.6.0", "cpuCount": 16, "cpuModel": "Apple M4 Max", "totalMemory": "64.00 GB", "freeMemory": "0.15 GB" }, "node": { "version": "v22.19.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.8.2" }, "frameworks": [ { "name": "fastify", "version": "^5.5.0" } ], "databases": [ { "name": "pg", "version": "^8.16.0" }, { "name": "kysely", "version": "^0.28.8" } ], "betterAuth": { "version": "v1.4.0-beta.20", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, emailVerification: { sendOnSignUp: true, async sendVerificationEmail(data, request) { sendVerificationEmail(data, request); }, }, plugins: [ emailOTP({ overrideDefaultEmailVerification: true, async sendVerificationOTP(data, request) { // }, }), ] }); ``` ### Additional context It seems the `defu` does not override the values in destination object at [context/helpers.tx#L33](https://github.com/better-auth/better-auth/blob/31cb70c7e9358466c5faf9fd94ea1764b1721f54/packages/better-auth/src/context/helpers.ts#L33) and treats plugin returned options as defaults However, when `emailVerification.sendVerificationEmail` is not defined, everything works fine.
GiteaMirror added the lockedbug labels 2026-04-15 17:46:45 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 12, 2025):

(tag @better-auth-agent if you would like an answer from the Agent)

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3523948092 --> @better-auth-agent[bot] commented on GitHub (Nov 12, 2025): (tag @better-auth-agent if you would like an answer from the Agent) <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

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

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

You are correct—this is a bug in how Better Auth merges plugin and user config options. The overrideDefaultEmailVerification option in the emailOTP plugin is supposed to suppress the default verification email and only send the OTP, but due to the way options are merged (using defu), the plugin's override is treated as a default rather than a true override. As a result, if you define emailVerification.sendVerificationEmail in your config, both the default verification email and the OTP are sent, which is not the intended behavior. This is confirmed by the code and test cases, and has been reported by other users as well (example).

A temporary workaround is to avoid defining sendVerificationEmail in your config when using overrideDefaultEmailVerification: true in the plugin. This will prevent the default verification email from being sent, and only the OTP will go out. However, this is not a true fix—it's a limitation of the current merging logic.

There is currently no merged fix for this in the latest versions, and the merging logic in context/helpers.ts will need to be updated to allow plugin overrides to take precedence over user config for cases like this. Until then, the workaround above is the best option.

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:3523959417 --> @dosubot[bot] commented on GitHub (Nov 12, 2025): <!-- Greeting --> Hi @Shridhad! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> You are correct—this is a bug in how Better Auth merges plugin and user config options. The `overrideDefaultEmailVerification` option in the `emailOTP` plugin is supposed to suppress the default verification email and only send the OTP, but due to the way options are merged (using `defu`), the plugin's override is treated as a default rather than a true override. As a result, if you define `emailVerification.sendVerificationEmail` in your config, both the default verification email and the OTP are sent, which is not the intended behavior. This is confirmed by the code and test cases, and has been reported by other users as well ([example](https://github.com/better-auth/better-auth/issues/3494)). A temporary workaround is to avoid defining `sendVerificationEmail` in your config when using `overrideDefaultEmailVerification: true` in the plugin. This will prevent the default verification email from being sent, and only the OTP will go out. However, this is not a true fix—it's a limitation of the current merging logic. There is currently no merged fix for this in the latest versions, and the merging logic in `context/helpers.ts` will need to be updated to allow plugin overrides to take precedence over user config for cases like this. Until then, the workaround above is the best option. <!-- 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/fdb9c47c-071e-4b62-a7c4-b6edfb04ec88?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/fdb9c47c-071e-4b62-a7c4-b6edfb04ec88?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/fdb9c47c-071e-4b62-a7c4-b6edfb04ec88?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/fdb9c47c-071e-4b62-a7c4-b6edfb04ec88?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/fdb9c47c-071e-4b62-a7c4-b6edfb04ec88?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/fdb9c47c-071e-4b62-a7c4-b6edfb04ec88?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/fdb9c47c-071e-4b62-a7c4-b6edfb04ec88?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/5936)
Author
Owner

@Shridhad commented on GitHub (Nov 12, 2025):

Seems the use of defu is intentional to give precedence to configuration defined creating betterAuth than plugin options.

The option emailOTP. overrideDefaultEmailVerification sounds it should override the default config, maybe a better suitable name will avoid the confusion.

Closing as its intentional behavior and caused by duplicate config options.

<!-- gh-comment-id:3524053830 --> @Shridhad commented on GitHub (Nov 12, 2025): Seems the use of `defu` is intentional to give precedence to configuration defined creating `betterAuth` than plugin options. The option `emailOTP. overrideDefaultEmailVerification` sounds it should override the default config, maybe a better suitable name will avoid the confusion. Closing as its intentional behavior and caused by duplicate config options.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19012