[GH-ISSUE #3494] Verification Mail Sent on Login Attempt w/ sendOnSignIn: false #9623

Closed
opened 2026-04-13 05:11:42 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @Norcim133 on GitHub (Jul 20, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3494

Originally assigned to: @bytaesu on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. User signs up with emailVerification required
  2. User ignores the verification mail
  3. User tries to sign in without verifying
  4. Error returns correctly, informing user they need to verfiy
  5. Email is automatically sent (despite saying false below) <-- Why?

Current vs. Expected behavior

I expected that with sendOnSignIn: false (see below) it would not automatically send a new verification email automatically.

Here is what the docs say:

sendOnSignIn: Send verification email automatically on sign in when the user's email is not verified (default: false)

This would allow me to use my own button to trigger a manual verification mail.

Instead, an email goes out automatically.

/auth.ts
emailVerification: {
        sendOnSignUp: true,
        sendOnSignIn: false, //Shouldn't automatically send new verification mail on attempted sign-in 
        autoSignInAfterVerification: true, ...

emailAndPassword: {
        enabled: true,
        requireEmailVerification: true,...

What version of Better Auth are you using?

1.2.12

Provide environment information

- OS: OSX
- Chrome

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

Backend

Auth config (if applicable)

// src/lib/auth.ts

import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
import { Pool } from "pg";
import Stripe from 'stripe';
import { sendEmail } from './email.js';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const pool = new Pool({
    connectionString: process.env.DATABASE_URL,
});

// This is the core server-side instance of Better Auth.
export const auth = betterAuth({
    database: pool,
    baseURL: process.env.BETTER_AUTH_URL,
    emailVerification: {
        sendOnSignUp: true,
        sendOnSignIn: false, //Shouldn't automatically send new verification mail on attempted sign-in 
        autoSignInAfterVerification: true, 
        sendVerificationEmail: async ({ user, url }) => {
            await sendEmail({
                to: user.email,
                subject: '...',
                html: '...')}
    },
    emailAndPassword: {
        enabled: true,
        requireEmailVerification: true, // Doesn't require RE-verification each time... but won't log in until done once
        sendResetPassword: async ({ user, url }) => {
            await sendEmail({
                to: user.email,
                subject: '...',
                html: '...'
            })
        }
    },
    user: {
        additionalFields: {
            agreedToTerms: {
                type: "boolean",
                // This makes it a required field for the signUp function.
                // The library will throw an error if it's missing or false.
                required: true,
                defaultValue: false,
                // This must be true, allowing the value to come from the client.
                input: true,
            }
        }
    },
    // Automatically create linked stripe account with option to announce or notify to server
    hooks: {
        after: createAuthMiddleware(async (ctx) => {
            // We only care about successful sign-up events
            if (ctx.path.startsWith('/sign-up')) {
                // The `ctx.context` object contains the newly created session and user
                const newSession = ctx.context.newSession;
                
                if (newSession && newSession.user) {
                    const { user } = newSession;
                    console.log(`New user signed up: ${user.email}. Creating Stripe customer.`);
                    try {
                        const newCustomer = await stripe.customers.create({
                            email: user.email,
                            name: user.name,
                            metadata: {
                                appUserId: user.id,
                            },
                        });

                        // Now, update our new user record with the Stripe customer ID
                        await pool.query(
                            'UPDATE "user" SET "stripeCustomerId" = $1 WHERE id = $2',
                            [newCustomer.id, user.id]
                        );
                    } catch (error) {
                        // Log the error. The sign-up is already complete, so we won't block the user.
                        console.error(`Failed to create Stripe customer for user ${user.id}:`, error);
                    }
                }
            }
        }),
    },
});

Additional context

No response

Originally created by @Norcim133 on GitHub (Jul 20, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3494 Originally assigned to: @bytaesu on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. User signs up with emailVerification required 2. User ignores the verification mail 3. User tries to sign in without verifying 4. Error returns correctly, informing user they need to verfiy 5. Email is automatically sent (despite saying false below) **<-- Why?** ### Current vs. Expected behavior I expected that with sendOnSignIn: false (see below) it would not automatically send a new verification email automatically. Here is what the docs say: > sendOnSignIn: Send verification email automatically on sign in when the user's email is not verified (default: false) This would allow me to use my own button to trigger a manual verification mail. Instead, an email goes out automatically. ``` /auth.ts emailVerification: { sendOnSignUp: true, sendOnSignIn: false, //Shouldn't automatically send new verification mail on attempted sign-in autoSignInAfterVerification: true, ... emailAndPassword: { enabled: true, requireEmailVerification: true,... ``` ### What version of Better Auth are you using? 1.2.12 ### Provide environment information ```bash - OS: OSX - Chrome ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript // src/lib/auth.ts import { betterAuth } from "better-auth"; import { createAuthMiddleware } from "better-auth/api"; import { Pool } from "pg"; import Stripe from 'stripe'; import { sendEmail } from './email.js'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); // This is the core server-side instance of Better Auth. export const auth = betterAuth({ database: pool, baseURL: process.env.BETTER_AUTH_URL, emailVerification: { sendOnSignUp: true, sendOnSignIn: false, //Shouldn't automatically send new verification mail on attempted sign-in autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }) => { await sendEmail({ to: user.email, subject: '...', html: '...')} }, emailAndPassword: { enabled: true, requireEmailVerification: true, // Doesn't require RE-verification each time... but won't log in until done once sendResetPassword: async ({ user, url }) => { await sendEmail({ to: user.email, subject: '...', html: '...' }) } }, user: { additionalFields: { agreedToTerms: { type: "boolean", // This makes it a required field for the signUp function. // The library will throw an error if it's missing or false. required: true, defaultValue: false, // This must be true, allowing the value to come from the client. input: true, } } }, // Automatically create linked stripe account with option to announce or notify to server hooks: { after: createAuthMiddleware(async (ctx) => { // We only care about successful sign-up events if (ctx.path.startsWith('/sign-up')) { // The `ctx.context` object contains the newly created session and user const newSession = ctx.context.newSession; if (newSession && newSession.user) { const { user } = newSession; console.log(`New user signed up: ${user.email}. Creating Stripe customer.`); try { const newCustomer = await stripe.customers.create({ email: user.email, name: user.name, metadata: { appUserId: user.id, }, }); // Now, update our new user record with the Stripe customer ID await pool.query( 'UPDATE "user" SET "stripeCustomerId" = $1 WHERE id = $2', [newCustomer.id, user.id] ); } catch (error) { // Log the error. The sign-up is already complete, so we won't block the user. console.error(`Failed to create Stripe customer for user ${user.id}:`, error); } } } }), }, }); ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-13 05:11:42 -05:00
Author
Owner

@Bekacru commented on GitHub (Jul 20, 2025):

update to latest

<!-- gh-comment-id:3093209108 --> @Bekacru commented on GitHub (Jul 20, 2025): update to latest
Author
Owner

@arturovv commented on GitHub (Aug 3, 2025):

It can be reproduced in the latest version 1.3.4.

<!-- gh-comment-id:3148323787 --> @arturovv commented on GitHub (Aug 3, 2025): It can be reproduced in the latest version 1.3.4.
Author
Owner

@frectonz commented on GitHub (Sep 4, 2025):

@Norcim133 I have looked at the code and have verified that sendOnSignIn is handled correctly, meaning an email is not sent if it is set to false, which is the default. I have created a PR to add tests for this in #4431

Are you still facing this problem? If so do you have a minimal reproduction for it.

<!-- gh-comment-id:3254173690 --> @frectonz commented on GitHub (Sep 4, 2025): @Norcim133 I have looked at the code and have verified that `sendOnSignIn` is handled correctly, meaning an email is not sent if it is set to `false`, which is the default. I have created a PR to add tests for this in #4431 Are you still facing this problem? If so do you have a minimal reproduction for it.
Author
Owner

@iltan987 commented on GitHub (Nov 5, 2025):

I use 1.3.34 and have issues related to this. I use email otp plugin. Email is being sent upon sign-up automatically even if

emailVerification: {
  sendOnSignUp: false,
},

is set and for emailOtp:

plugins: [
  emailOTP({
    sendVerificationOnSignUp: false,
    async sendVerificationOTP({ email, otp, type }) {
      // email send functionality here
    },
    overrideDefaultEmailVerification: true,
  }),
],
<!-- gh-comment-id:3491455232 --> @iltan987 commented on GitHub (Nov 5, 2025): I use 1.3.34 and have issues related to this. I use email otp plugin. Email is being sent upon sign-up automatically even if ```ts emailVerification: { sendOnSignUp: false, }, ``` is set and for emailOtp: ```ts plugins: [ emailOTP({ sendVerificationOnSignUp: false, async sendVerificationOTP({ email, otp, type }) { // email send functionality here }, overrideDefaultEmailVerification: true, }), ], ```
Author
Owner

@bytaesu commented on GitHub (Jan 24, 2026):

Let me check this 🧐

<!-- gh-comment-id:3795315199 --> @bytaesu commented on GitHub (Jan 24, 2026): Let me check this 🧐
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9623