[GH-ISSUE #7117] afterEmailVerification not called by email OTP after email is verified #19364

Closed
opened 2026-04-15 18:22:39 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @dngpng on GitHub (Jan 4, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/7117

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Enable the Email OTP plugin and define an 'afterEmailVerification' function in better-auth configuration, e.g.

     export const auth = betterAuth({
       emailAndPassword: {  
         enabled: true
       },
       emailVerification: {
         sendOnSignUp: true,
    
         afterEmailVerification: async (user) => {
           await onUserEmailVerified(user);
         },
       },
       plugins: [
         emailOTP({
           otpLength: 6,
           expiresIn: 300, // 5 minutes
           sendVerificationOnSignUp: true,
           overrideDefaultEmailVerification: true,
           async sendVerificationOTP({ email, otp, type }) {
             void sendOTPEmail(email, otp, type);
           },
         }),
       ]
     });
    
  2. Trigger a email verification

  3. afterEmailVerification is not called.

Current vs. Expected behavior

Issue

There are currently two hook functions supposed to be run after email gets verified.

  • onEmailVerification
  • afterEmailVerification

which is already confusing. The docs currently only mention the latter in the email verification section, although onEmailVerification is widely used in code base.

As can be seen in Email OTP's processing after email gets verified, only the onEmailVerification from option is called.

https://github.com/better-auth/better-auth/blob/c8110d8881f00c1ca7af8b746de6887b48302e45/packages/better-auth/src/plugins/email-otp/routes.ts#L511-L513

Expected

  • Keep only one hook variant, or
  • If both are needed, update doc to reflect that
  • If both are supported, update Email OTP plugin to call the hook with a fallback mechanism, so either will be called if defined.

What version of Better Auth are you using?

1.4.10

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:49 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T6000",
    "release": "24.5.0",
    "cpuCount": 10,
    "cpuModel": "Apple M1 Pro",
    "totalMemory": "32.00 GB",
    "freeMemory": "0.13 GB"
  },
  "node": {
    "version": "v22.15.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "^15.3.8"
    },
    {
      "name": "react",
      "version": "^19.1.4"
    }
  ],
  "databases": [
    {
      "name": "postgres",
      "version": "^3.4.4"
    },
    {
      "name": "drizzle",
      "version": "^0.45.1"
    }
  ],
  "betterAuth": {
    "version": "^1.4.10",
    "config": {
      "baseURL": "http://localhost:3000",
      "disabledPaths": [
        "/token"
      ],
      "emailAndPassword": {
        "enabled": true,
        "revokeSessionsOnPasswordReset": true,
        "password": {}
      },
      "emailVerification": {
        "sendOnSignUp": true
      },
      "user": {
        "changeEmail": {
          "enabled": true,
          "updateEmailWithoutVerification": false
        },
        "deleteUser": {
          "enabled": true
        }
      },
      "socialProviders": {
        "github": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]"
        },
        "google": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]"
        }
      },
      "account": {
        "accountLinking": {
          "enabled": true,
          "trustedProviders": [
            "google",
            "github"
          ]
        }
      },
      "databaseHooks": {
        "user": {
          "create": {},
          "delete": {}
        }
      },
      "plugins": [
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": "Failed to create user",
              "USER_ALREADY_EXISTS": "User already exists.",
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.",
              "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself",
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users",
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users",
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users",
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]",
              "BANNED_USER": "You have been banned from this application",
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user",
              "NO_DATA_TO_UPDATE": "No data to update",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users",
              "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself",
              "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "You are not allowed to set a non-existent role value",
              "YOU_CANNOT_IMPERSONATE_ADMINS": "You cannot impersonate admins",
              "INVALID_ROLE_TYPE": "Invalid role type"
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            }
          }
        },
        {
          "name": "two-factor",
          "config": {
            "id": "two-factor",
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "user": {
                "fields": {
                  "twoFactorEnabled": {
                    "type": "boolean",
                    "required": false,
                    "defaultValue": false,
                    "input": false
                  }
                }
              },
              "twoFactor": {
                "fields": {
                  "secret": {
                    "type": "string",
                    "required": true,
                    "returned": false,
                    "index": true
                  },
                  "backupCodes": {
                    "type": "string",
                    "required": true,
                    "returned": false
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "returned": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "index": true
                  }
                }
              }
            },
            "rateLimit": [
              {
                "window": 10,
                "max": 3
              }
            ],
            "$ERROR_CODES": {
              "OTP_NOT_ENABLED": "OTP not enabled",
              "OTP_HAS_EXPIRED": "OTP has expired",
              "TOTP_NOT_ENABLED": "TOTP not enabled",
              "TWO_FACTOR_NOT_ENABLED": "Two factor isn't enabled",
              "BACKUP_CODES_NOT_ENABLED": "Backup codes aren't enabled",
              "INVALID_BACKUP_CODE": "Invalid backup code",
              "INVALID_CODE": "Invalid code",
              "TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE": "Too many attempts. Please request a new code.",
              "INVALID_TWO_FACTOR_COOKIE": "Invalid two factor cookie"
            }
          }
        },
        {
          "name": "next-cookies",
          "config": {
            "id": "next-cookies",
            "hooks": {
              "after": [
                {}
              ]
            }
          }
        },
        {
          "name": "jwt",
          "config": {
            "id": "jwt",
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "jwks": {
                "fields": {
                  "publicKey": {
                    "type": "string",
                    "required": true
                  },
                  "privateKey": {
                    "type": "string",
                    "required": true
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "expiresAt": {
                    "type": "date",
                    "required": false
                  }
                }
              }
            }
          }
        },
        {
          "name": "oauthProvider",
          "config": {
            "id": "oauthProvider",
            "options": {
              "codeExpiresIn": 600,
              "accessTokenExpiresIn": 3600,
              "m2mAccessTokenExpiresIn": 3600,
              "refreshTokenExpiresIn": 2592000,
              "allowUnauthenticatedClientRegistration": true,
              "allowDynamicClientRegistration": true,
              "disableJwtPlugin": false,
              "storeClientSecret": "[REDACTED]",
              "storeTokens": "hashed",
              "grantTypes": [
                "authorization_code",
                "client_credentials",
                "refresh_token"
              ],
              "loginPage": "/sign-in",
              "consentPage": "/consent",
              "resource": "http://localhost:3000",
              "validAudiences": [
                "http://localhost:3000",
                "http://localhost:3000/",
                "http://localhost:3000/mcp",
                "http://localhost:3000/mcp/"
              ],
              "scopes": [
                "openid",
                "profile",
                "email"
              ],
              "clientRegistrationDefaultScopes": [
                "openid",
                "profile",
                "email"
              ],
              "claims": [
                "sub",
                "iss",
                "aud",
                "exp",
                "iat",
                "sid",
                "scope",
                "azp",
                "email",
                "email_verified",
                "name",
                "picture",
                "family_name",
                "given_name"
              ],
              "clientRegistrationAllowedScopes": [
                "openid",
                "profile",
                "email"
              ]
            },
            "hooks": {
              "before": [
                {}
              ],
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "schema": {
              "oauthClient": {
                "modelName": "oauthClient",
                "fields": {
                  "clientId": {
                    "type": "string",
                    "unique": true,
                    "required": true
                  },
                  "clientSecret": {
                    "type": "string",
                    "required": false
                  },
                  "disabled": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false
                  },
                  "skipConsent": {
                    "type": "boolean",
                    "required": false
                  },
                  "enableEndSession": {
                    "type": "boolean",
                    "required": false
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": false
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "createdAt": {
                    "type": "date",
                    "required": false
                  },
                  "updatedAt": {
                    "type": "date",
                    "required": false
                  },
                  "name": {
                    "type": "string",
                    "required": false
                  },
                  "uri": {
                    "type": "string",
                    "required": false
                  },
                  "icon": {
                    "type": "string",
                    "required": false
                  },
                  "contacts": {
                    "type": "string[]",
                    "required": false
                  },
                  "tos": {
                    "type": "string",
                    "required": false
                  },
                  "policy": {
                    "type": "string",
                    "required": false
                  },
                  "softwareId": {
                    "type": "string",
                    "required": false
                  },
                  "softwareVersion": {
                    "type": "string",
                    "required": false
                  },
                  "softwareStatement": {
                    "type": "string",
                    "required": false
                  },
                  "redirectUris": {
                    "type": "string[]",
                    "required": true
                  },
                  "postLogoutRedirectUris": {
                    "type": "string[]",
                    "required": false
                  },
                  "tokenEndpointAuthMethod": {
                    "type": "string",
                    "required": false
                  },
                  "grantTypes": {
                    "type": "string[]",
                    "required": false
                  },
                  "responseTypes": {
                    "type": "string[]",
                    "required": false
                  },
                  "public": {
                    "type": "boolean",
                    "required": false
                  },
                  "type": {
                    "type": "string",
                    "required": false
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "metadata": {
                    "type": "json",
                    "required": false
                  }
                }
              },
              "oauthRefreshToken": {
                "fields": {
                  "token": {
                    "type": "string",
                    "required": true
                  },
                  "clientId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "oauthClient",
                      "field": "clientId"
                    }
                  },
                  "sessionId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "session",
                      "field": "id",
                      "onDelete": "set null"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "expiresAt": {
                    "type": "date"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "revoked": {
                    "type": "date",
                    "required": false
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": true
                  }
                }
              },
              "oauthAccessToken": {
                "modelName": "oauthAccessToken",
                "fields": {
                  "token": {
                    "type": "string",
                    "unique": true
                  },
                  "clientId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "oauthClient",
                      "field": "clientId"
                    }
                  },
                  "sessionId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "session",
                      "field": "id",
                      "onDelete": "set null"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "refreshId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "oauthRefreshToken",
                      "field": "id"
                    }
                  },
                  "expiresAt": {
                    "type": "date"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": true
                  }
                }
              },
              "oauthConsent": {
                "modelName": "oauthConsent",
                "fields": {
                  "clientId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "oauthClient",
                      "field": "clientId"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": true
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  }
                }
              }
            }
          }
        },
        {
          "name": "last-login-method",
          "config": {
            "id": "last-login-method",
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "user": {
                "fields": {
                  "lastLoginMethod": {
                    "type": "string",
                    "input": false,
                    "required": false,
                    "fieldName": "lastLoginMethod"
                  }
                }
              }
            },
            "options": {
              "storeInDatabase": true
            }
          }
        },
        {
          "name": "email-otp",
          "config": {
            "id": "email-otp",
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "$ERROR_CODES": {
              "OTP_EXPIRED": "OTP expired",
              "INVALID_OTP": "Invalid OTP",
              "TOO_MANY_ATTEMPTS": "Too many attempts"
            },
            "rateLimit": [
              {
                "window": 60,
                "max": 3
              },
              {
                "window": 60,
                "max": 3
              },
              {
                "window": 60,
                "max": 3
              },
              {
                "window": 60,
                "max": 3
              }
            ],
            "options": {
              "otpLength": 6,
              "expiresIn": 300,
              "sendVerificationOnSignUp": true,
              "overrideDefaultEmailVerification": true
            }
          }
        }
      ]
    }
  }
}

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

Documentation, Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
  emailVerification: {
    sendOnSignUp: true,
    
    afterEmailVerification: async (user) => {
      await onUserEmailVerified(user);
    },
  },
  plugins: [
    emailOTP({
      otpLength: 6,
      expiresIn: 300, // 5 minutes
      sendVerificationOnSignUp: true,
      overrideDefaultEmailVerification: true,
      async sendVerificationOTP({ email, otp, type }) {
        void sendOTPEmail(email, otp, type);
      },
    }),
  ]
});

Additional context

No response

Originally created by @dngpng on GitHub (Jan 4, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/7117 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Enable the Email OTP plugin and define an 'afterEmailVerification' function in better-auth configuration, e.g. ``` export const auth = betterAuth({ emailAndPassword: { enabled: true }, emailVerification: { sendOnSignUp: true, afterEmailVerification: async (user) => { await onUserEmailVerified(user); }, }, plugins: [ emailOTP({ otpLength: 6, expiresIn: 300, // 5 minutes sendVerificationOnSignUp: true, overrideDefaultEmailVerification: true, async sendVerificationOTP({ email, otp, type }) { void sendOTPEmail(email, otp, type); }, }), ] }); ``` 2. Trigger a email verification 3. `afterEmailVerification` is not called. ### Current vs. Expected behavior ## Issue There are currently two hook functions supposed to be run after email gets verified. - `onEmailVerification` - `afterEmailVerification` which is already confusing. The docs currently only mention the latter [in the email verification section](https://www.better-auth.com/docs/concepts/email#callback-after-successful-email-verification), although `onEmailVerification` is widely used in code base. As can be seen in Email OTP's processing after email gets verified, only the `onEmailVerification` from option is called. https://github.com/better-auth/better-auth/blob/c8110d8881f00c1ca7af8b746de6887b48302e45/packages/better-auth/src/plugins/email-otp/routes.ts#L511-L513 ## Expected - Keep only one hook variant, or - If both are needed, update doc to reflect that - If both are supported, update Email OTP plugin to call the hook with a fallback mechanism, so either will be called if defined. ### What version of Better Auth are you using? 1.4.10 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:49 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T6000", "release": "24.5.0", "cpuCount": 10, "cpuModel": "Apple M1 Pro", "totalMemory": "32.00 GB", "freeMemory": "0.13 GB" }, "node": { "version": "v22.15.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": [ { "name": "next", "version": "^15.3.8" }, { "name": "react", "version": "^19.1.4" } ], "databases": [ { "name": "postgres", "version": "^3.4.4" }, { "name": "drizzle", "version": "^0.45.1" } ], "betterAuth": { "version": "^1.4.10", "config": { "baseURL": "http://localhost:3000", "disabledPaths": [ "/token" ], "emailAndPassword": { "enabled": true, "revokeSessionsOnPasswordReset": true, "password": {} }, "emailVerification": { "sendOnSignUp": true }, "user": { "changeEmail": { "enabled": true, "updateEmailWithoutVerification": false }, "deleteUser": { "enabled": true } }, "socialProviders": { "github": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]" }, "google": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]" } }, "account": { "accountLinking": { "enabled": true, "trustedProviders": [ "google", "github" ] } }, "databaseHooks": { "user": { "create": {}, "delete": {} } }, "plugins": [ { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": "Failed to create user", "USER_ALREADY_EXISTS": "User already exists.", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.", "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself", "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions", "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users", "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users", "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions", "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users", "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]", "BANNED_USER": "You have been banned from this application", "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user", "NO_DATA_TO_UPDATE": "No data to update", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users", "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself", "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "You are not allowed to set a non-existent role value", "YOU_CANNOT_IMPERSONATE_ADMINS": "You cannot impersonate admins", "INVALID_ROLE_TYPE": "Invalid role type" }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } } } }, { "name": "two-factor", "config": { "id": "two-factor", "endpoints": {}, "hooks": { "after": [ {} ] }, "schema": { "user": { "fields": { "twoFactorEnabled": { "type": "boolean", "required": false, "defaultValue": false, "input": false } } }, "twoFactor": { "fields": { "secret": { "type": "string", "required": true, "returned": false, "index": true }, "backupCodes": { "type": "string", "required": true, "returned": false }, "userId": { "type": "string", "required": true, "returned": false, "references": { "model": "user", "field": "id" }, "index": true } } } }, "rateLimit": [ { "window": 10, "max": 3 } ], "$ERROR_CODES": { "OTP_NOT_ENABLED": "OTP not enabled", "OTP_HAS_EXPIRED": "OTP has expired", "TOTP_NOT_ENABLED": "TOTP not enabled", "TWO_FACTOR_NOT_ENABLED": "Two factor isn't enabled", "BACKUP_CODES_NOT_ENABLED": "Backup codes aren't enabled", "INVALID_BACKUP_CODE": "Invalid backup code", "INVALID_CODE": "Invalid code", "TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE": "Too many attempts. Please request a new code.", "INVALID_TWO_FACTOR_COOKIE": "Invalid two factor cookie" } } }, { "name": "next-cookies", "config": { "id": "next-cookies", "hooks": { "after": [ {} ] } } }, { "name": "jwt", "config": { "id": "jwt", "endpoints": {}, "hooks": { "after": [ {} ] }, "schema": { "jwks": { "fields": { "publicKey": { "type": "string", "required": true }, "privateKey": { "type": "string", "required": true }, "createdAt": { "type": "date", "required": true }, "expiresAt": { "type": "date", "required": false } } } } } }, { "name": "oauthProvider", "config": { "id": "oauthProvider", "options": { "codeExpiresIn": 600, "accessTokenExpiresIn": 3600, "m2mAccessTokenExpiresIn": 3600, "refreshTokenExpiresIn": 2592000, "allowUnauthenticatedClientRegistration": true, "allowDynamicClientRegistration": true, "disableJwtPlugin": false, "storeClientSecret": "[REDACTED]", "storeTokens": "hashed", "grantTypes": [ "authorization_code", "client_credentials", "refresh_token" ], "loginPage": "/sign-in", "consentPage": "/consent", "resource": "http://localhost:3000", "validAudiences": [ "http://localhost:3000", "http://localhost:3000/", "http://localhost:3000/mcp", "http://localhost:3000/mcp/" ], "scopes": [ "openid", "profile", "email" ], "clientRegistrationDefaultScopes": [ "openid", "profile", "email" ], "claims": [ "sub", "iss", "aud", "exp", "iat", "sid", "scope", "azp", "email", "email_verified", "name", "picture", "family_name", "given_name" ], "clientRegistrationAllowedScopes": [ "openid", "profile", "email" ] }, "hooks": { "before": [ {} ], "after": [ {} ] }, "endpoints": {}, "schema": { "oauthClient": { "modelName": "oauthClient", "fields": { "clientId": { "type": "string", "unique": true, "required": true }, "clientSecret": { "type": "string", "required": false }, "disabled": { "type": "boolean", "defaultValue": false, "required": false }, "skipConsent": { "type": "boolean", "required": false }, "enableEndSession": { "type": "boolean", "required": false }, "scopes": { "type": "string[]", "required": false }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id" } }, "createdAt": { "type": "date", "required": false }, "updatedAt": { "type": "date", "required": false }, "name": { "type": "string", "required": false }, "uri": { "type": "string", "required": false }, "icon": { "type": "string", "required": false }, "contacts": { "type": "string[]", "required": false }, "tos": { "type": "string", "required": false }, "policy": { "type": "string", "required": false }, "softwareId": { "type": "string", "required": false }, "softwareVersion": { "type": "string", "required": false }, "softwareStatement": { "type": "string", "required": false }, "redirectUris": { "type": "string[]", "required": true }, "postLogoutRedirectUris": { "type": "string[]", "required": false }, "tokenEndpointAuthMethod": { "type": "string", "required": false }, "grantTypes": { "type": "string[]", "required": false }, "responseTypes": { "type": "string[]", "required": false }, "public": { "type": "boolean", "required": false }, "type": { "type": "string", "required": false }, "referenceId": { "type": "string", "required": false }, "metadata": { "type": "json", "required": false } } }, "oauthRefreshToken": { "fields": { "token": { "type": "string", "required": true }, "clientId": { "type": "string", "required": true, "references": { "model": "oauthClient", "field": "clientId" } }, "sessionId": { "type": "string", "required": false, "references": { "model": "session", "field": "id", "onDelete": "set null" } }, "userId": { "type": "string", "required": true, "references": { "model": "user", "field": "id" } }, "referenceId": { "type": "string", "required": false }, "expiresAt": { "type": "date" }, "createdAt": { "type": "date" }, "revoked": { "type": "date", "required": false }, "scopes": { "type": "string[]", "required": true } } }, "oauthAccessToken": { "modelName": "oauthAccessToken", "fields": { "token": { "type": "string", "unique": true }, "clientId": { "type": "string", "required": true, "references": { "model": "oauthClient", "field": "clientId" } }, "sessionId": { "type": "string", "required": false, "references": { "model": "session", "field": "id", "onDelete": "set null" } }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id" } }, "referenceId": { "type": "string", "required": false }, "refreshId": { "type": "string", "required": false, "references": { "model": "oauthRefreshToken", "field": "id" } }, "expiresAt": { "type": "date" }, "createdAt": { "type": "date" }, "scopes": { "type": "string[]", "required": true } } }, "oauthConsent": { "modelName": "oauthConsent", "fields": { "clientId": { "type": "string", "required": true, "references": { "model": "oauthClient", "field": "clientId" } }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id" } }, "referenceId": { "type": "string", "required": false }, "scopes": { "type": "string[]", "required": true }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } } } } }, { "name": "last-login-method", "config": { "id": "last-login-method", "hooks": { "after": [ {} ] }, "schema": { "user": { "fields": { "lastLoginMethod": { "type": "string", "input": false, "required": false, "fieldName": "lastLoginMethod" } } } }, "options": { "storeInDatabase": true } } }, { "name": "email-otp", "config": { "id": "email-otp", "endpoints": {}, "hooks": { "after": [ {} ] }, "$ERROR_CODES": { "OTP_EXPIRED": "OTP expired", "INVALID_OTP": "Invalid OTP", "TOO_MANY_ATTEMPTS": "Too many attempts" }, "rateLimit": [ { "window": 60, "max": 3 }, { "window": 60, "max": 3 }, { "window": 60, "max": 3 }, { "window": 60, "max": 3 } ], "options": { "otpLength": 6, "expiresIn": 300, "sendVerificationOnSignUp": true, "overrideDefaultEmailVerification": true } } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Documentation, Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, emailVerification: { sendOnSignUp: true, afterEmailVerification: async (user) => { await onUserEmailVerified(user); }, }, plugins: [ emailOTP({ otpLength: 6, expiresIn: 300, // 5 minutes sendVerificationOnSignUp: true, overrideDefaultEmailVerification: true, async sendVerificationOTP({ email, otp, type }) { void sendOTPEmail(email, otp, type); }, }), ] }); ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-15 18:22:40 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Jan 4, 2026):

It appears the issue you're experiencing with the afterEmailVerification not being called may stem from the confusion between the onEmailVerification and afterEmailVerification hooks. As noted in existing GitHub discussions, there might be some cases where the hooks are not triggered as expected during email verification processes, especially related to differing scenarios like OAuth vs. traditional email sign-up methods.

Next Steps:

  1. Check the Implementation: Make sure that your better-auth configuration is correct and that both hooks are intended to be called appropriately. The documentation may need an update to clarify how and when each hook is used.

  2. Refactor or Raise an Issue: Consider refactoring your email verification flow or raising an issue so the maintainers can address potential bugs or clarify documentation regarding the use of these hooks.

Would you need further assistance with configuring the hooks, or is there something specific you’d like to clarify about their implementation?

If you need more help, tag @better-auth-agent in a comment so I can respond.

Diagram • Discord • GitHub

Diagram Join Star

<!-- gh-comment-id:3707519728 --> @better-auth-agent[bot] commented on GitHub (Jan 4, 2026): It appears the issue you're experiencing with the `afterEmailVerification` not being called may stem from the confusion between the `onEmailVerification` and `afterEmailVerification` hooks. As noted in existing GitHub discussions, there might be some cases where the hooks are not triggered as expected during email verification processes, especially related to differing scenarios like OAuth vs. traditional email sign-up methods. ### Next Steps: 1. **Check the Implementation**: Make sure that your `better-auth` configuration is correct and that both hooks are intended to be called appropriately. The documentation may need an update to clarify how and when each hook is used. 2. **Refactor or Raise an Issue**: Consider refactoring your email verification flow or raising an issue so the maintainers can address potential bugs or clarify documentation regarding the use of these hooks. Would you need further assistance with configuring the hooks, or is there something specific you’d like to clarify about their implementation? _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) • [Discord](https://discord.gg/fG2XXEuQX3) • [GitHub](https://github.com/Skyvern-AI/Skyvern) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/fG2XXEuQX3) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/Skyvern-AI/Skyvern)
Author
Owner

@GautamBytes commented on GitHub (Jan 4, 2026):

Looking into it!

<!-- gh-comment-id:3708041966 --> @GautamBytes commented on GitHub (Jan 4, 2026): Looking into it!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19364