You launch your SaaS. Users sign up. Three days later, your first support email arrives: "I forgot my password -- can you reset it for me?"
If you have not built a password reset flow, your options are bad: manually update the database, or lose the user. Neither is acceptable once you have paying customers.
Password reset is the auth feature founders always ship "later" -- until a locked-out user arrives and later becomes now.
A forgot-password flow has four steps:
That is it. No magic, no third-party service. The Next.js SaaS Boilerplate already has everything the flow needs: JWT for signing tokens, Resend for sending the email, Drizzle ORM for reading the user record, and bcryptjs for hashing the new password.
You have two options for the reset token:
Database token -- store a random string in the users table, check it on verify, delete it after use. Simple to understand but requires a migration: two new columns (reset_token and reset_token_expires_at) on your users table.
Signed JWT -- encode the user ID and expiry in a signed token, verify the signature on the callback. No extra DB column needed. The token is self-contained and verifiable with your existing JWT_SECRET.
For most early-stage SaaS products, the JWT approach is the right call. You skip the migration, and a 15-minute expiry limits the risk window. The tradeoff: you cannot revoke a JWT before it expires without maintaining a blocklist. If your product handles sensitive data and your threat model requires immediate revocation, use a database token instead. For everything else, the signed JWT is faster to ship and easier to maintain.
POST /api/auth/forgot-password does three things:
One critical detail: always return a 200 response whether or not the email exists. Returning 404 for unknown emails tells an attacker which email addresses are in your database. Return the same response either way.
POST /api/auth/reset-password:
If the token is invalid or expired, return a 400. The user sees an error on the reset page and can request a new link.
Use the existing Resend integration in lib/email.ts. A password reset email is a transactional email with one link -- the same pattern as your welcome email or email verification. The boilerplate's ResetPasswordEmail template accepts an appName and a reset URL. Pass your token as a query parameter and you are done.
Keep the email short: one clear link, a note that it expires in 15 minutes, and a line saying if you did not request this, ignore it. The template already handles this.
/forgot-password -- email input form (React Hook Form + Zod)
/reset-password -- new password form (reads token from URL query)
POST /api/auth/forgot-password -- send the email
POST /api/auth/reset-password -- verify token, update password
Both routes follow the same thin-route pattern already in place: validate with Zod, call the service, return the response. Business logic lives in the service layer, not the route handler. See the authentication docs for the full pattern.
Password reset pairs naturally with email verification (already in the boilerplate) and builds on the same JWT helper you use for session tokens. If you have already read through email verification in Next.js or magic link login, the pattern will feel familiar -- sign a short-lived token, send a link, verify on callback.
The difference is scope: a reset token should only ever update the password. Keep it narrow. Do not reuse reset tokens for any other purpose.
Most founders add password reset after the first locked-out user emails them. That user almost always churns because the recovery experience is slow or manual.
Add the flow before you launch. It takes a few hours with the boilerplate's auth helpers already in place, and it is one of those features users never notice -- until it saves them.
Get started with the Next.js SaaS Boilerplate and ship a complete auth setup, password reset included, in a day.