Next.js Authentication Boilerplate: Email Verification, Roles & Sessions

admin admin1 views

Authentication is the foundation of every SaaS. Get it wrong and you face security vulnerabilities, user experience problems, and weeks of debugging. Get it right from day one and it disappears — it just works.

This guide covers what production-grade authentication looks like in a Next.js 14 App Router application.

What Production Auth Actually Requires

A login form is not an authentication system. A complete auth system includes: registration with email verification, login with secure session management, password reset with time-limited tokens, role-based access control, protection for admin routes, blocking disposable email addresses, and proper error handling for every failure case.

Most tutorials cover the login form. The rest is what makes the difference between a toy project and a real product.

Email Verification: Why It Is Non-Negotiable

Without email verification, anyone can register with a fake email address. This creates problems: you cannot contact your users, your email deliverability suffers, and malicious users can register freely without accountability.

The correct implementation: on registration, create the user with emailVerified set to null. Send a verification email with a time-limited token. When the user clicks the link, set emailVerified to the current timestamp. Block unverified users from accessing protected features.

In NextAuth.js with a custom auth.ts, you check emailVerified in the authorize callback and throw a custom error (EMAIL_NOT_VERIFIED) that the client can handle to show the appropriate message.

Role-Based Access Control

Most SaaS products need at least two roles: regular users and administrators. The role is stored on the User model in your database and included in the NextAuth session.

Every admin Route Handler must call getServerSession and check that session.user.role === 'ADMIN'. Return a 403 response for unauthorized requests. Never rely on client-side checks alone — they can be bypassed.

Session Management: Email vs ID

A subtle but important architectural decision: use session.user.email as the primary identifier for database lookups, not session.user.id.

Why? NextAuth does not always include the user ID in the session by default, and the behavior can be inconsistent depending on the adapter and session strategy. Email is always present and is a stable unique identifier.

Every user lookup in your Route Handlers should use findUnique({ where: { email: session.user.email } }).

Blocking Disposable Emails

Disposable email services (mailinator, tempmail, and hundreds of others) are used to create fake accounts, abuse free trials, and avoid bans. Block them at registration by maintaining a list of known disposable domains and rejecting any email address whose domain appears in the list.

A list of 200+ disposable domains provides good coverage without false positives.

The Password Reset Flow

Password reset requires: a form to request the reset (enter your email), a Route Handler that creates a time-limited token and sends the reset email, a page to enter the new password (with the token in the URL), and a Route Handler that verifies the token, checks it has not expired, updates the password, and invalidates the token.

The token must be single-use and must expire (15-60 minutes is standard). Store a hashed version of the token in the database, not the raw token.

AliyaSaas Authentication

AliyaSaas implements all of this correctly: email verification with blocking, role-based access control, session-by-email lookups, 200+ disposable email domains blocked, and a complete password reset flow. The auth configuration is in src/lib/auth.ts and is fully documented.

You can read the code, understand every decision, and customize it for your needs. Or you can deploy it as-is and have production-grade authentication running immediately.

Related articles