Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

Password Reset in Your Next.js SaaS -- Forgot Password Flow With JWT and Resend

September 15, 2026
nextjsauthresenddrizzle-ormsaas

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.

What the flow looks like, step by step

A forgot-password flow has four steps:

  1. User enters their email on a /forgot-password page
  2. Your app sends a short-lived signed link to that email via Resend
  3. User clicks the link, lands on /reset-password, and sets a new password
  4. The link expires so nobody can reuse it

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.

A database token vs a signed JWT -- which should you use?

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.

The request endpoint

POST /api/auth/forgot-password does three things:

  1. Look up the user by email using Drizzle ORM
  2. Sign a JWT with the user ID and a 15-minute expiry
  3. Send the reset link via Resend

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.

The reset endpoint

POST /api/auth/reset-password:

  1. Verifies the JWT signature and checks expiry using the existing JWT helper in lib/auth.ts
  2. Hashes the new password with bcryptjs
  3. Updates the user record in Drizzle ORM

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.

The reset email

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.

The pages and routes

/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.

Where this fits with the rest of your auth setup

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.

Ship it before someone asks for it

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.