[GH-ISSUE #3761] The sendVerificationOTP in the Email OTP plugin dosen't provide the user object as the og link based method #18345

Closed
opened 2026-04-15 16:46:54 -05:00 by GiteaMirror · 8 comments
Owner

Originally created by @Fefedu973 on GitHub (Aug 2, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3761

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

The sendVerificationOTP in the Email OTP plugin dosen't provide the user object as the og link based method. This is preventing us to create a custom email with the user's name for example. We should be able to find a workaround by trying to search the db for the user based on the email but this is extra steps and less convenient way to do this.

Current vs. Expected behavior

I expect to have access to the user object in this function

What version of Better Auth are you using?

1.3.4

Provide environment information

windows 11

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { emailOTP } from "better-auth/plugins";
import { organization } from "better-auth/plugins";
import { resend } from "./resend";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { expo } from "@better-auth/expo";
import { db } from "../db";
import * as schema from "../db/schema/auth";

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "sqlite",

    schema: schema,
  }),
  appName: "Open Backtest",
  trustedOrigins: [process.env.CORS_ORIGIN || "", "my-better-t-app://"],

  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID || "",
      clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
    },
    microsoft: {
      clientId: process.env.MICROSOFT_CLIENT_ID || "",
      clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "",
    },
  },

  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    sendResetPassword: async ({ user, url, token }) => {
      // Send reset password email
    },
  },

  emailVerification: {
    sendOnSignUp: true,
    autoSignInAfterVerification: true,
  },

  account: {
    accountLinking: {
      enabled: true,
    },
  },

  user: {
    changeEmail: {
      enabled: true,
      sendChangeEmailVerification: async ({ user, newEmail, url, token }) => {
        // Send change email verification
      },
    },
    deleteUser: {
      enabled: true,
      sendDeleteAccountVerification: async ({ user, url, token }) => {
        // Send delete account verification
      },
    },
    // additionalFields: {
    //   partner: {
    //     type: "boolean",
    //     required: true,
    //     input: false,
    //     defaultValue: false,
    //   },
    // },
  },

  secret: process.env.BETTER_AUTH_SECRET,
  baseURL: process.env.BETTER_AUTH_URL,
  plugins: [
    expo(),
    emailOTP({
      overrideDefaultEmailVerification: true,
      sendVerificationOnSignUp: true,
      async sendVerificationOTP({ email, otp, type }) {
        if (type === "email-verification") {
          // SEND EMAIL BUT NO USER INFO
        }
        else if (type === "sign-in") { }
        else if (type === "forget-password") { }
      },
    }),
    organization(),
  ],
});

Additional context

No response

Originally created by @Fefedu973 on GitHub (Aug 2, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3761 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce The sendVerificationOTP in the Email OTP plugin dosen't provide the user object as the og link based method. This is preventing us to create a custom email with the user's name for example. We should be able to find a workaround by trying to search the db for the user based on the email but this is extra steps and less convenient way to do this. ### Current vs. Expected behavior I expect to have access to the user object in this function ### What version of Better Auth are you using? 1.3.4 ### Provide environment information ```bash windows 11 ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { emailOTP } from "better-auth/plugins"; import { organization } from "better-auth/plugins"; import { resend } from "./resend"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { expo } from "@better-auth/expo"; import { db } from "../db"; import * as schema from "../db/schema/auth"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite", schema: schema, }), appName: "Open Backtest", trustedOrigins: [process.env.CORS_ORIGIN || "", "my-better-t-app://"], socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID || "", clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", }, microsoft: { clientId: process.env.MICROSOFT_CLIENT_ID || "", clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "", }, }, emailAndPassword: { enabled: true, requireEmailVerification: true, sendResetPassword: async ({ user, url, token }) => { // Send reset password email }, }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, }, account: { accountLinking: { enabled: true, }, }, user: { changeEmail: { enabled: true, sendChangeEmailVerification: async ({ user, newEmail, url, token }) => { // Send change email verification }, }, deleteUser: { enabled: true, sendDeleteAccountVerification: async ({ user, url, token }) => { // Send delete account verification }, }, // additionalFields: { // partner: { // type: "boolean", // required: true, // input: false, // defaultValue: false, // }, // }, }, secret: process.env.BETTER_AUTH_SECRET, baseURL: process.env.BETTER_AUTH_URL, plugins: [ expo(), emailOTP({ overrideDefaultEmailVerification: true, sendVerificationOnSignUp: true, async sendVerificationOTP({ email, otp, type }) { if (type === "email-verification") { // SEND EMAIL BUT NO USER INFO } else if (type === "sign-in") { } else if (type === "forget-password") { } }, }), organization(), ], }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-15 16:46:55 -05:00
Author
Owner

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

You're correct—the Email OTP plugin's sendVerificationOTP function only receives { email, otp, type } and does not provide the user object. This is intentional and matches the current implementation; the user object isn't available in this context because OTP flows can be triggered before a user exists (like during sign-up) or when only an email is present see source.

The common workaround is to manually query your database for the user by email inside sendVerificationOTP if you need user details for customizing emails see example. This is a known limitation and has been discussed in several issues see discussion.

If you need richer user context (like for personalized emails), the recommended approach is to use the core emailVerification config's sendVerificationEmail at the top level, which does provide the user object see recommendation.

Let me know if you want a code snippet for the manual lookup workaround!

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:3146739013 --> @dosubot[bot] commented on GitHub (Aug 2, 2025): <!-- Answer --> You're correct—the Email OTP plugin's sendVerificationOTP function only receives { email, otp, type } and does not provide the user object. This is intentional and matches the current implementation; the user object isn't available in this context because OTP flows can be triggered before a user exists (like during sign-up) or when only an email is present [see source](https://github.com/better-auth/better-auth/blob/987be984b4d840a409ca1e0d6098ee0f3303bcb6/packages/better-auth/src/plugins/email-otp/index.ts). The common workaround is to manually query your database for the user by email inside sendVerificationOTP if you need user details for customizing emails [see example](https://github.com/better-auth/better-auth/issues/2165). This is a known limitation and has been discussed in several issues [see discussion](https://github.com/better-auth/better-auth/issues/2160). If you need richer user context (like for personalized emails), the recommended approach is to use the core emailVerification config's sendVerificationEmail at the top level, which does provide the user object [see recommendation](https://github.com/better-auth/better-auth/issues/3570). Let me know if you want a code snippet for the manual lookup workaround! <!-- 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/94a460ab-a3b6-44b0-a4ab-b0dc07108b64?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/94a460ab-a3b6-44b0-a4ab-b0dc07108b64?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/94a460ab-a3b6-44b0-a4ab-b0dc07108b64?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/94a460ab-a3b6-44b0-a4ab-b0dc07108b64?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/94a460ab-a3b6-44b0-a4ab-b0dc07108b64?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/94a460ab-a3b6-44b0-a4ab-b0dc07108b64?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/94a460ab-a3b6-44b0-a4ab-b0dc07108b64?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/3761)
Author
Owner

@Fefedu973 commented on GitHub (Aug 2, 2025):

@dosu for the richer context can i use the old sendVerificationEmail function that will call the send verification otp but with the additonal infos ? Will this still be called despite my current config ? can you give me code snipets ?

<!-- gh-comment-id:3146743084 --> @Fefedu973 commented on GitHub (Aug 2, 2025): @dosu for the richer context can i use the old sendVerificationEmail function that will call the send verification otp but with the additonal infos ? Will this still be called despite my current config ? can you give me code snipets ?
Author
Owner

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

No, with your current config (overrideDefaultEmailVerification: true in the emailOTP plugin), the core sendVerificationEmail function will NOT be called—only sendVerificationOTP runs, and it does not receive the user object, just { email, otp, type } (reference, plugin code).

If you want richer context (like the user object), you have two options:

  1. Remove overrideDefaultEmailVerification from the plugin and use the core emailVerification.sendVerificationEmail at the top level. This function receives the full user object and is best for personalized emails (example).

  2. If you must use OTP and need user info, manually fetch the user by email inside sendVerificationOTP:

import { db } from "../db"; // adjust import as needed
import { users } from "../db/schema/auth"; // adjust import as needed

emailOTP({
  // overrideDefaultEmailVerification: true,
  async sendVerificationOTP({ email, otp, type }) {
    // Manually fetch user for richer context
    const user = await db.query.users.findFirst({ where: { email } });
    // Now you can use user?.name, etc. in your email template
    await sendCustomOTPEmail({ email, otp, user });
  },
});

There’s no built-in way to chain both flows or pass the user object directly to sendVerificationOTP (plugin code). If you want the best of both worlds, you have to do the manual lookup.

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:3146745221 --> @dosubot[bot] commented on GitHub (Aug 2, 2025): <!-- Answer --> No, with your current config (`overrideDefaultEmailVerification: true` in the `emailOTP` plugin), the core `sendVerificationEmail` function will NOT be called—only `sendVerificationOTP` runs, and it does not receive the user object, just `{ email, otp, type }` ([reference](https://github.com/better-auth/better-auth/issues/3570), [plugin code](https://github.com/better-auth/better-auth/blob/987be984b4d840a409ca1e0d6098ee0f3303bcb6/packages/better-auth/src/plugins/email-otp/index.ts)). If you want richer context (like the user object), you have two options: 1. **Remove `overrideDefaultEmailVerification` from the plugin and use the core `emailVerification.sendVerificationEmail` at the top level.** This function receives the full user object and is best for personalized emails ([example](https://github.com/better-auth/better-auth/issues/3570)). 2. **If you must use OTP and need user info, manually fetch the user by email inside `sendVerificationOTP`:** ```ts import { db } from "../db"; // adjust import as needed import { users } from "../db/schema/auth"; // adjust import as needed emailOTP({ // overrideDefaultEmailVerification: true, async sendVerificationOTP({ email, otp, type }) { // Manually fetch user for richer context const user = await db.query.users.findFirst({ where: { email } }); // Now you can use user?.name, etc. in your email template await sendCustomOTPEmail({ email, otp, user }); }, }); ``` There’s no built-in way to chain both flows or pass the user object directly to `sendVerificationOTP` ([plugin code](https://github.com/better-auth/better-auth/blob/987be984b4d840a409ca1e0d6098ee0f3303bcb6/packages/better-auth/src/plugins/email-otp/index.ts)). If you want the best of both worlds, you have to do the manual lookup. <!-- 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/281806fd-3167-4743-962e-ffde965f7fd7?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/281806fd-3167-4743-962e-ffde965f7fd7?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/281806fd-3167-4743-962e-ffde965f7fd7?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/281806fd-3167-4743-962e-ffde965f7fd7?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/281806fd-3167-4743-962e-ffde965f7fd7?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/281806fd-3167-4743-962e-ffde965f7fd7?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/281806fd-3167-4743-962e-ffde965f7fd7?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/3761)
Author
Owner

@Fefedu973 commented on GitHub (Aug 5, 2025):

No... Idk what it is

<!-- gh-comment-id:3154778451 --> @Fefedu973 commented on GitHub (Aug 5, 2025): No... Idk what it is
Author
Owner

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

Hi, @Fefedu973. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You reported that the sendVerificationOTP function in the Email OTP plugin lacks access to the user object, limiting email customization.
  • It was clarified that this is intentional because OTP flows can happen before a user exists.
  • The suggested workaround is to manually query the user by email within sendVerificationOTP for richer context or disable overrideDefaultEmailVerification to use the core sendVerificationEmail function.
  • You asked about combining these approaches, but it was explained that only sendVerificationOTP runs with the current config, so manual user lookup is necessary.
  • There was also a side discussion about the TypeScript config setting exactOptionalPropertyTypes, which you were not aware of.

Next Steps:

  • Please confirm if this issue is still relevant with the latest version of better-auth; if so, you can keep the discussion open by commenting here.
  • Otherwise, I will automatically close this issue in 7 days.

Thanks for your understanding and contribution!

<!-- gh-comment-id:3486877479 --> @dosubot[bot] commented on GitHub (Nov 4, 2025): Hi, @Fefedu973. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary:** - You reported that the sendVerificationOTP function in the Email OTP plugin lacks access to the user object, limiting email customization. - It was clarified that this is intentional because OTP flows can happen before a user exists. - The suggested workaround is to manually query the user by email within sendVerificationOTP for richer context or disable overrideDefaultEmailVerification to use the core sendVerificationEmail function. - You asked about combining these approaches, but it was explained that only sendVerificationOTP runs with the current config, so manual user lookup is necessary. - There was also a side discussion about the TypeScript config setting exactOptionalPropertyTypes, which you were not aware of. **Next Steps:** - Please confirm if this issue is still relevant with the latest version of better-auth; if so, you can keep the discussion open by commenting here. - Otherwise, I will automatically close this issue in 7 days. Thanks for your understanding and contribution!
Author
Owner

@Fefedu973 commented on GitHub (Nov 11, 2025):

Please re-open

<!-- gh-comment-id:3517741521 --> @Fefedu973 commented on GitHub (Nov 11, 2025): Please re-open
Author
Owner

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

@Bekacru The user has requested to reopen this issue as it is still relevant. Could you please take a look and assist?

<!-- gh-comment-id:3517754587 --> @dosubot[bot] commented on GitHub (Nov 11, 2025): @Bekacru The user has requested to reopen this issue as it is still relevant. Could you please take a look and assist?
Author
Owner

@github-actions[bot] commented on GitHub (Apr 1, 2026):

This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.

<!-- gh-comment-id:4166557145 --> @github-actions[bot] commented on GitHub (Apr 1, 2026): This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18345