Configurable email validation via a global validation option
Is this suited for GitHub?
- Yes, this is suited for github
Is your feature request related to a problem? Please describe.
Better Auth validates email syntax with z.email() in core routes, several first-party plugins, and the CLI. Zod ships several email patterns on purpose, but an app can't pick one. The default rejects user&[email protected] even though & is plain ASCII and valid under RFC 5322. Other apps need Unicode addresses via z.regexes.unicodeEmail. Changing the default would push a new policy on everyone; the missing piece is a way for an app to replace it.
The username and phone-number plugins already have usernameValidator and phoneNumberValidator. Email spans core and multiple plugins, so it needs a global home rather than a plugin option.
Describe the solution you'd like
A validation option on BetterAuthOptions, starting with email:
betterAuth({
validation: {
email: (email) =>
z.email({ pattern: z.regexes.rfc5322Email }).safeParse(email).success,
},
});- Same
(value) => boolean | Promise<boolean>contract as the two existing validators. - When unset, addresses are still validated with today's
z.email()policy. - When set, it replaces the default check rather than stacking on it, so stricter and looser policies both work.
- The callback receives the lowercased address. Most routes already lowercase before validating; sign-up and sign-in would move to match.
falserejects the address. Error compatibility for schema-backed routes is part of the design point below.- Applies to every core and first-party route and the
create-adminCLI command that validates a submitted address, including a caller-supplied SIWE email. Not applied to already-signed token claims (verify-email) or to addresses produced bycreatePlaceholderEmailfor anonymous, SIWE, and Entra ID accounts. - The namespace leaves room for the username and phone-number validators to move under it later. That's not part of this request.
Name and placement are illustrative.
One design point to settle. Six routes validate email in body schemas constructed without access to the resolved global options: sign-up, request-password-reset, send-verification-email, change-email, magic-link, and SIWE. Two ways to handle it:
- Construct the affected endpoint schemas from the resolved auth options during initialization. Preserves schema-level validation but requires refactoring endpoint construction.
- Accept a string in the body and validate in the handler, as sign-in does today. Smaller, but those six routes would return
INVALID_EMAILinstead of a body-validation error, and OpenAPI email metadata would need to be preserved explicitly.
Describe alternatives you've considered
- Change the default pattern. Changes policy for every app and doesn't allow stricter ones.
- Ask Zod to change its default. Zod offers the patterns as a choice by design; it wouldn't make Better Auth's policy configurable.
- Request hooks. They can inspect or modify the request context, but the endpoint's fixed
z.email()schema still runs afterward. Replacing validation this way would mean rewriting or short-circuiting built-in flows. - Replace endpoint schemas wholesale. Far broader, and it exposes endpoint internals as config.
- Patch the installed package. Requires maintaining a patch across upgrades.
Additional context
This follows the discussion in #10920, where @bytaesu requested a cleaner global abstraction and asked that it be discussed before targeting a minor release.
The previous implementation used request-scoped schema refinement. This proposal doesn't assume that approach.
Source: better-auth/better-auth