Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Configure email verification, implement password reset flows, set password policies, and customise hashing algorithms for Better Auth email/password authentication. Use when users need to set up login, sign-in, sign-up, credential authentication, or password security with Better Auth.
.claude/skills/aiskillstore-email-and-password-best-practices/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -31% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 12% | 0% |
| case-09 | ✗→✓ | ▲ Improved | -5% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 136% | 0% |
| case-11 | ✗→✓ | ▲ Improved | -25% | 0% |
emailAndPassword: { enabled: true }emailVerification.sendVerificationEmailsendResetPassword for password reset flowsnpx @better-auth/cli@latest migrateConfigure emailVerification.sendVerificationEmail to verify user email addresses.
tsimport { betterAuth } from "better-auth"; import { sendEmail } from "./email"; // your email sending function export const auth = betterAuth({ emailVerification: { sendVerificationEmail: async ({ user, url, token }, request) => { await sendEmail({ to: user.email, subject: "Verify your email address", text: `Click the link to verify your email: ${url}`, }); }, }, });
Note: The url parameter contains the full verification link. The token is available if you need to build a custom verification URL.
For stricter security, enable emailAndPassword.requireEmailVerification to block sign-in until the user verifies their email. When enabled, unverified users will receive a new verification email on each sign-in attempt.
tsexport const auth = betterAuth({ emailAndPassword: { requireEmailVerification: true, }, });
Note: This requires sendVerificationEmail to be configured and only applies to email/password sign-ins.
Implement client-side validation for immediate user feedback and reduced server load.
Always use absolute URLs (including the origin) for callback URLs in sign-up and sign-in requests. This prevents Better Auth from needing to infer the origin, which can cause issues when your backend and frontend are on different domains.
tsconst { data, error } = await authClient.signUp.email({ callbackURL: "https://example.com/callback", // absolute URL with origin });
Provide sendResetPassword in the email and password config to enable password resets.
tsimport { betterAuth } from "better-auth"; import { sendEmail } from "./email"; // your email sending function export const auth = betterAuth({ emailAndPassword: { enabled: true, // Custom email sending function to send reset-password email sendResetPassword: async ({ user, url, token }, request) => { void sendEmail({ to: user.email, subject: "Reset your password", text: `Click the link to reset your password: ${url}`, }); }, // Optional event hook onPasswordReset: async ({ user }, request) => { // your logic here console.log(`Password for user ${user.email} has been reset.`); }, }, });
Built-in protections: background email sending (timing attack prevention), dummy operations on invalid requests, constant response messages regardless of user existence.
On serverless platforms, configure a background task handler:
tsexport const auth = betterAuth({ advanced: { backgroundTasks: { handler: (promise) => { // Use platform-specific methods like waitUntil waitUntil(promise); }, }, }, });
Tokens expire after 1 hour by default. Configure with resetPasswordTokenExpiresIn (in seconds):
tsexport const auth = betterAuth({ emailAndPassword: { enabled: true, resetPasswordTokenExpiresIn: 60 * 30, // 30 minutes }, });
Tokens are single-use — deleted immediately after successful reset.
Enable revokeSessionsOnPasswordReset to invalidate all existing sessions on password reset:
tsexport const auth = betterAuth({ emailAndPassword: { enabled: true, revokeSessionsOnPasswordReset: true, }, });
Password length limits (configurable):
tsexport const auth = betterAuth({ emailAndPassword: { enabled: true, minPasswordLength: 12, maxPasswordLength: 256, }, });
Call requestPasswordReset to send the reset link. Triggers the sendResetPassword function from your config.
tsconst data = await auth.api.requestPasswordReset({ body: { email: "john.doe@example.com", // required redirectTo: "https://example.com/reset-password", }, });
Or authClient:
tsconst { data, error } = await authClient.requestPasswordReset({ email: "john.doe@example.com", // required redirectTo: "https://example.com/reset-password", });
Note: While the email is required, we also recommend configuring the redirectTo for a smoother user experience.
Default: scrypt (Node.js native, no external dependencies).
To use Argon2id or another algorithm, provide custom hash and verify functions:
tsimport { betterAuth } from "better-auth"; import { hash, verify, type Options } from "@node-rs/argon2"; const argon2Options: Options = { memoryCost: 65536, // 64 MiB timeCost: 3, // 3 iterations parallelism: 4, // 4 parallel lanes outputLen: 32, // 32 byte output algorithm: 2, // Argon2id variant }; export const auth = betterAuth({ emailAndPassword: { enabled: true, password: { hash: (password) => hash(password, argon2Options), verify: ({ password, hash: storedHash }) => verify(storedHash, password, argon2Options), }, }, });
Note: If you switch hashing algorithms on an existing system, users with passwords hashed using the old algorithm won't be able to sign in. Plan a migration strategy if needed.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 24,682 | 10,241 | -59% | 1 | 1 | 0% | 3,662 | 2,528 | -31% | 0 | 0 | — |
case-02 | fail→pass | 16,832 | 5,955 | -65% | 1 | 1 | 0% | 2,386 | 2,677 | +12% | 0 | 0 | — |
case-03 | pass→pass | 12,292 | 12,431 | +1% | 1 | 1 | 0% | 1,805 | 2,234 | +24% | 0 | 0 | — |
case-04 | pass→pass | 14,654 | 7,997 | -45% | 1 | 1 | 0% | 1,354 | 1,977 | +46% | 0 | 0 | — |
case-05 | pass→pass | 13,119 | 8,651 | -34% | 1 | 1 | 0% | 1,446 | 1,925 | +33% | 0 | 0 | — |
case-06 | pass→pass | 18,336 | 3,092 | -83% | 1 | 1 | 0% | 1,299 | 1,759 | +35% | 0 | 0 | — |
case-07 | pass→pass | 12,305 | 10,437 | -15% | 1 | 1 | 0% | 1,387 | 2,247 | +62% | 0 | 0 | — |
case-08 | pass→pass | 11,827 | 14,028 | +19% | 1 | 1 | 0% | 1,771 | 2,255 | +27% | 0 | 0 | — |
case-09 | fail→pass | 22,688 | 14,225 | -37% | 1 | 1 | 0% | 2,805 | 2,655 | -5% | 0 | 0 | — |
case-10 | fail→pass | 11,698 | 20,623 | +76% | 1 | 1 | 0% | 967 | 2,279 | +136% | 0 | 0 | — |
case-11 | fail→pass | 20,071 | 16,444 | -18% | 1 | 1 | 0% | 2,731 | 2,044 | -25% | 0 | 0 | — |
case-12 | fail→pass | 38,579 | 8,728 | -77% | 1 | 1 | 0% | 1,926 | 1,859 | -3% | 0 | 0 | — |
case-13 | fail→pass | 17,180 | 10,046 | -42% | 1 | 1 | 0% | 2,067 | 2,309 | +12% | 0 | 0 | — |
case-14 | pass→pass | 13,318 | 7,807 | -41% | 1 | 1 | 0% | 1,526 | 1,923 | +26% | 0 | 0 | — |
case-15 | fail→pass | 10,418 | 7,001 | -33% | 1 | 1 | 0% | 883 | 1,671 | +89% | 0 | 0 | — |
case-16 | pass→pass | 13,037 | 23,019 | +77% | 1 | 1 | 0% | 1,310 | 1,722 | +31% | 0 | 0 | — |
case-17 | pass→pass | 22,898 | 10,536 | -54% | 1 | 1 | 0% | 2,209 | 2,329 | +5% | 0 | 0 | — |
case-18 | pass→pass | 12,275 | 6,792 | -45% | 1 | 1 | 0% | 1,417 | 1,721 | +21% | 0 | 0 | — |
case-19 | pass→pass | 17,120 | 8,426 | -51% | 1 | 1 | 0% | 1,504 | 1,907 | +27% | 0 | 0 | — |
case-20 | pass→pass | 30,080 | 24,317 | -19% | 1 | 1 | 0% | 1,732 | 2,437 | +41% | 0 | 0 | — |
case-21 | pass→pass | 34,963 | 16,107 | -54% | 1 | 1 | 0% | 3,520 | 3,677 | +4% | 0 | 0 | — |
case-22 | pass→pass | 13,171 | 27,621 | +110% | 1 | 1 | 0% | 1,550 | 2,784 | +80% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +36 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.