Skip to content
BlogNext.jsTechnical article

How to Build Email Verification in Next.js (Auth.js) — Production Guide

Learn how to implement a secure email verification flow in Next.js using Auth.js, Prisma, and Resend. Production-ready approach for SaaS apps.

PP

Patrice Parny

Founder of PyColors

April 17, 202610 min read
Why this article matters

This article captures a concrete implementation decision from building PyColors and turns it into a reusable technical pattern for developers building serious SaaS products.

How to Build Email Verification in Next.js (Auth.js)

If you're building a real SaaS product, email verification is not optional.

Without it, you risk:

  • fake accounts
  • spam users
  • broken onboarding flows
  • unreliable user data

In this guide, you’ll learn how to implement a production-ready email verification flow using:

  • Next.js (App Router)
  • Auth.js
  • Prisma
  • Resend

This is the exact architecture used in the PyColors Starter Pro.


What We’re Building

A complete email verification flow:

  1. User registers with email + password
  2. A verification email is sent
  3. User clicks a secure token link
  4. Token is validated
  5. Account is marked as verified

Why Email Verification Matters in SaaS

In a real SaaS, email verification impacts:

  • onboarding conversion
  • security
  • billing reliability
  • support workflows

If you skip it early, you’ll pay for it later.


Step 1 — Extend Your Database Schema

You need two things:

  • a way to know if a user is verified
  • a way to store verification tokens

With Prisma:

model User {
  id             String   @id @default(cuid())
  email          String   @unique
  emailVerified  DateTime?
  passwordHash   String?
}

model UserToken {
  id        String   @id @default(cuid())
  email     String
  token     String   @unique
  type      TokenType
  expiresAt DateTime
  createdAt DateTime @default(now())
}

enum TokenType {
  EMAIL_VERIFICATION
  PASSWORD_RESET
}

Step 2 — Generate a Verification Token

import crypto from "crypto";

export function generateToken() {
  return crypto.randomBytes(32).toString("hex");
}

Step 3 — Store Token

await prisma.userToken.create({
  data: {
    email: user.email,
    token,
    type: "EMAIL_VERIFICATION",
    expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
  },
});

Step 4 — Send Email

import { Resend } from "resend";

const resend = new Resend(process.env.AUTH_RESEND_API_KEY);

await resend.emails.send({
  from: process.env.AUTH_EMAIL_FROM!,
  to: user.email,
  subject: "Verify your email",
  html: `<a href="${process.env.NEXT_PUBLIC_APP_URL}/verify-email?token=${token}">Verify</a>`,
});

Step 5 — Verify Token

const tokenRecord = await prisma.userToken.findUnique({
  where: { token },
});

if (!tokenRecord || tokenRecord.expiresAt < new Date()) {
  throw new Error("Invalid or expired token");
}

Step 6 — Mark Verified

await prisma.user.update({
  where: { email: tokenRecord.email },
  data: { emailVerified: new Date() },
});

await prisma.userToken.delete({
  where: { token },
});

Step 7 — Protect App

if (!user.emailVerified) {
  throw new Error("Email not verified");
}

Common Mistakes

  • no expiration
  • reusable tokens
  • weak tokens
  • blocking login too early

Want This Already Built?

👉 https://pycolors.io/starters


Final Thoughts

Build it once. Build it right.

Starter Pro

Turn this article into shipping leverage

Explore the PyColors offer connected to this implementation pattern.

Keep exploring

Related articles around the same technical and product surface.

Next.jsFeatured

Tailwind Not Detecting Classes from node_modules? Fix for Next.js + UI Libraries

Fix Tailwind CSS not applying styles from node_modules in Next.js. Learn how to configure content paths for UI libraries.

March 17, 20268 min read
SaaS ArchitectureFeatured

Why I Stopped Overengineering SaaS Starters

I removed complexity from my SaaS starter to make it more useful, easier to ship, and closer to what developers actually need.

March 19, 20268 min read