A broken password reset is a support ticket factory.
Forgot password flows fail in predictable ways: reset emails that don't deliver, links that expire too quickly, tokens that work more than once, or redirects that go to the wrong place. Fixing the flow and moving to a reliable email provider is usually a day's work.
Password reset flow that doesn't work reliably — emails not delivered, tokens expired, links broken, or users getting errors when trying to reset
Password reset failures cluster into categories:
Email not delivered:
The most common cause. Using SMTP or an unconfigured email service without SPF/DKIM results in emails landing in spam or being rejected. Check spam folder first. If consistently failing: migrate to a proper transactional email provider (Resend, SendGrid) with domain authentication.
Token expired before use:
Reset tokens should expire. 1 hour is the standard window. If they're expiring in minutes, the expiry calculation has a bug. If they're expiring before the email is even received, there's a delivery latency issue.
Token reuse not prevented:
Reset tokens should be single-use. After the password is reset, invalidate the token. If the token remains valid after use, anyone who accesses the email later could still use the link.
Wrong URL in email:
Hardcoded localhost:3000 URLs in the reset email link (production email still pointing to development URL). Fix: set NEXT_PUBLIC_BASE_URL environment variable and use it when generating the reset link.
The correct implementation:
// Generate reset token
const token = crypto.randomBytes(32).toString('hex');
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
// Store in DB
await db.update(users).set({
passwordResetToken: hashedToken,
passwordResetExpires: new Date(Date.now() + 60 * 60 * 1000) // 1 hour
}).where(eq(users.email, email));
// Send email with raw token (not the hash)
const resetUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/reset-password?token=${token}`;
When verifying: hash the submitted token, compare to the stored hash.
Reliable forgot password flow with proper token expiry, single-use tokens, and email delivery via a transactional email provider
Token generation fix
(crypto.randomBytes, proper expiry)
Single-use enforcement
(invalidate after use)
Email delivery fix
(Resend with domain authentication)
URL configuration
(environment-variable-driven base URL)
Rate limiting
on the forgot password endpoint
One honest number to start.
Fixed-scope, fixed-price. The number below is the starting point — final scope is built from your brief.
Reliable forgot password flow with proper token expiry, single-use tokens, and email delivery via a transactional email provider
Three steps, every time.
The same repeatable engagement on every project. No surprises, no mystery, no billable ambiguity.
Brief & discovery.
We send you questions, then get on a call. Output: a written scope with every step, feature, and integration listed.
Build & ship.
Fixed schedule, weekly reviews. No scope creep unless you change the scope — and if you do, we reprice it transparently.
Warranty & retainer.
30-day warranty on every launch. Most clients stay on a monthly retainer for ongoing features and maintenance.
Why Fixed-Price Matters Here
Password reset fixes are a defined checklist. Fixed-price.
Questions, answered.
Clerk and NextAuth.js handle password reset flows, including email delivery, token management, and the reset UI. If the application doesn't already use one of these, migrating to Clerk is often easier than debugging a broken custom implementation.
Show a specific message: "This account uses [Google/GitHub] to sign in. No password to reset." Avoid revealing whether the account exists if the email address might be sensitive.
Tell Ryel about your project.
Describe what you’re building and what outcome you need. You’ll have a written, fixed-price scope within the week.