

Better Auth Harmony

Better-Auth plugin for email & phone normalization and validation, blocking over 55,000 throwaway email domains.
Put identity rules in Better Auth
Better Auth Harmony adds email validation, email normalization and E.164 phone formatting to Better Auth. I wrote it so those rules live in the auth config instead of being copied into signup, sign-in, password reset and verification handlers.
Different email strings can reach the same inbox. With the default normalization, new.email+test@googlemail.com and newemail@gmail.com produce the same canonical value. Phone numbers have the same problem when punctuation or a local prefix changes how the number is written.
Add email checks with one plugin
Run npm install better-auth-harmony, then add emailHarmony() to an existing Better Auth config.
import { betterAuth } from 'better-auth';
import { emailHarmony } from 'better-auth-harmony';
export const auth = betterAuth({
plugins: [emailHarmony()],
});By default, the plugin checks syntax with validator.js and rejects domains listed by Mailchecker. The same check covers signup, sign-in, verification, password reset, email changes, magic links and email OTP.
The plugin keeps the address a user entered in email and writes its canonical value to a unique normalizedEmail field. Run npx @better-auth/cli migrate after adding it, or use the CLI’s generate command if the app manages its own migrations. The database constraint stops equivalent addresses from creating separate accounts.
Normalized sign-in is optional because it adds one database lookup per attempt. Set allowNormalizedSignin: true when users should be able to sign in with any equivalent form of their stored address.
Store phone numbers as E.164
Phone normalization works with Better Auth’s phone number plugin. Put phoneHarmony() after it in the plugin list.
import { betterAuth } from 'better-auth';
import { phoneNumber } from 'better-auth/plugins';
import { phoneHarmony } from 'better-auth-harmony';
export const auth = betterAuth({
plugins: [
phoneNumber({
sendOTP: ({ phoneNumber, code }) => sendSms(phoneNumber, code),
}),
phoneHarmony({ defaultCountry: 'GB' }),
],
});Harmony parses each value with libphonenumber-js and rewrites it before Better Auth sends an OTP, verifies a code, signs a user in or resets a password. A default country lets it parse local numbers without a calling code. The database stores the E.164 value directly, so there is no second phone field to keep in sync.
Keep authentication in Better Auth
Better Auth still owns passwords, sessions, OTPs and verification. Harmony only validates and normalizes the identifiers passed into those flows. It uses Better Auth’s request hooks, database hooks and schema extension, with no second account store.
If those defaults do not fit, the email validator can be asynchronous, both normalizers can be replaced and exported matchers can restrict the routes they cover. Basic setup needs none of that.