Implementing Passwordless Login in Next.js 13 with NextAuth.js + Prisma (2026)

Updated on December 27, 2025 6 minutes read


Passwordless login replaces passwords with a one-time sign-in step, typically a magic link sent to the user’s email. It reduces password resets, avoids weak-password problems, and keeps your UI simple.

This guide shows how to implement email magic-link authentication in a Next.js 13 (App Router) project using NextAuth.js v4 and Prisma (PostgreSQL / MySQL / SQLite).

Quick version choice for 2026

If you are on Next.js 13 (App Router)

Use NextAuth.js v4.

If you can upgrade to Next.js 14+

Use NextAuth.js v5 (Auth.js). v5 requires Next.js 14+, so it’s not the best fit if you must stay on Next.js 13. (See Appendix.)

What you will build

You will implement:

  • A built-in email sign-in form at GET /api/auth/signin.
  • A NextAuth Route Handler at app/api/auth/[...nextauth]/route.t...s.
  • Prisma-backed tables for:
    • User
    • Account
    • Session
    • VerificationToken

This is enough to test end-to-end magic links locally and deploy using a real SMTP provider.

Prerequisites

You’ll need:

  • Node.js + a package manager (npm / pnpm / Yarn)
  • A database connection string (Postgres / MySQL / SQLite)
  • SMTP credentials for sending emails
    • Gmail can work for demos (use an App Password), but production should use a dedicated provider (Postmark, SendGrid, Mailgun, etc.)

1) Create a fresh Next.js 13 app (App Router)

Pin to Next.js 13 so you don’t accidentally scaffold a newer Next.js version:

npx create-next-app@13 my-app --ts --eslint --app
cd my-app

If you already have an existing Next.js 13 app, skip this step.

2) Install dependencies (pin NextAuth to v4)

NextAuth v5 may be the default “latest” in 2026, so explicitly install v4:

# NextAuth v4 + email magic links (requires nodemailer)
npm i next-auth@4 nodemailer

# Prisma + NextAuth Prisma adapter (v4)
npm i @prisma/client @next-auth/prisma-adapter
npm i -D prisma

(Translate to pnpm add / yarn add if you prefer.)

3) Set up Prisma and your database

3.1 Initialize Prisma

npx prisma init

This creates:

  • prisma/schema.pris....ma.
  • .env (or you can use .env.local)

3.2 Add the NextAuth Prisma models

NextAuth’s Prisma adapter expects specific models.

Open prisma/schema.prisma and make sure it contains at least the following models.

Important: The schema differs slightly depending on your database connector.

  • For PostgreSQL / MySQL, you can use @db.Text on long token fields
  • For SQLite, remove @db.Text annotations

Option A: PostgreSQL / MySQL schema

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql" // or "mysql"
  url      = env("DATABASE_URL")
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.Text
  access_token      String? @db.Text
  expires_at        Int?
  token_type        StringScope     String?
  id_token          String? @db.Text
  session_state     String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  Email String?   @unique
  emailVerified DateTime?
  Image String?

  accounts Account[]
  sessions Session[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

Option B: SQLite schema

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  Scope String?
  id_token          String?
  session_state     String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  Email String?   @unique
  emailVerified DateTime?
  Image String?

  accounts Account[]
  sessions Session[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

3.3 Apply the migration

npx prisma migrate dev --name init

4) Configure environment variables

Open .env and set your database + email settings.

Example (PostgreSQL)

DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public"

EMAIL_SERVER="smtp://SMTP_USER:SMTP_PASSWORD@smtp.example.com:587"
EMAIL_FROM="noreply@yourdomain.com"

NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="replace-with-a-long-random-string"

Generate a good secret:

# macOS / Linux
openssl rand -base64 32

Notes:

  • In production, NEXTAUTH_URL must be your real HTTPS domain (g., https://example.com)
  • Don’t commit .env to git

In development, Next.js hot reload can create multiple Prisma clients unless you use a singleton.

Create lib/prisma.ts:

// lib/prisma.ts
import { PrismaClient } from "@prisma/client"

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    // log: ["query", "error", "warn"], // uncomment if you want prisma logs
  })

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

6) Add the NextAuth route handler (App Router)

Create this file:

app/api/auth/[...nextauth]/route.ts

Paste:

`` `ts import NextAuth, { type NextAuthOptions } from "next-auth" import EmailProvider from "next-auth/providers/e..mail." import { PrismaAdapter } from "@next-auth/prisma-adapter" import { prisma } from "@/lib/prisma"

// Important for email magic links: nodemailer uses Node.js APIs export const runtime = "nodejs"

export const authOptions: NextAuthOptions = { adapter: PrismaAdapter(prisma), secret: process.env.NEXTAUTH_SECRET, providers: [ EmailProvider({ server: process.env.EMAIL_SERVER, from: process.env.EMAIL_FROM, }), ], }

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }


Why `runtime = "nodejs"`?
- The email provider uses **nodemailer**, which relies on Node.js modules and can fail in Edge runtimes.

## 7) Run and test passwordless sign-in

Start the dev server:

```bash
npm run dev

Open:

  • http://localhost:3000/api/auth/signin

Enter an email address and submit. NextAuth will send a magic link. When you click the link:

  • NextAuth validates the token
  • It creates (or finds) the user in your DB
  • It creates a session and sets cookies

8) Common issues and fixes

Check:

  • SMTP credentials are correct
  • EMAIL_FROM is a valid sender address
  • Spam/Junk folder
  • Your SMTP provider allows sending from that address
  • Some providers require SPF/DKIM for good deliverability (production)

“Edge runtime error” mentioning Node.js modules (stream, etc.)

Make sure your auth route includes :

export const runtime = "nodejs"
`,``
Also, to verify you didn’t set a parent segment to Edge runtime.

### Prisma tables are missing
Usually one of these:

- `DATABASE_URL` is wrong
- You didn’t add the required NextAuth models
- You didn’t run the migration

Run:

```bash
npx prisma migrate dev

9) Production hardening checklist

Before shipping:

  • Use HTTPS on your production domain
  • NEXTAUTH_SECRET is long + random and not leaked
  • Use a dedicated SMTP provider with good deliverability
  • Configure sender domain correctly (SPF, DKIM, DMARC)
  • Add basic rate limiting to sign-in endpoints
  • Consider abuse protections (CAPTCHA, allowlist/denylist, etc.)
  • Confirm NEXTAUTH_URL matches your real domain exactly

Appendix: Next.js 14+ + NextAuth.js v5 (Auth.js) in 2026 (high level)

If you can upgrade to Next.js 14+, NextAuth v5 is App Router-first and typically uses a shared auth.ts setup with a cleaner handler export.

At a high level, you will:

  • Upgrade to Next.js 14+
  • Install Auth.js / NextAuth v5 (often via a beta tag depending on your timing)
  • Use @auth/prisma-adapter (v5) instead of @next-auth/prisma-adapter (v4)
  • Use the Nodemailer provider for magic links
  • Export route handlers from app/api/auth/[...nextauth]/route.ts.

Next steps

Explore Code Labs Academy's Web Development to see the curriculum and learning formats. If you want guidance on the best path for your goals, you can also Book a call.

Frequently Asked Questions

Do I need a database for email magic-link login?

Yes. Magic-link flows require a database to store verification tokens and (with an adapter) persist sessions/users. Prisma + Postgres is a common setup.

Can I use Gmail as the SMTP server?

For local demos, yes, if your account allows SMTP access. For production, use a dedicated email provider to improve deliverability and avoid personal-account limits.

Where should I put my NextAuth configuration in 2026 projects?

In newer setups, keep your main config in a root-level auth.ts file and connect it to the App Router route handler. Older projects may keep the full config inside the route file.

Why am I not receiving the magic link email?

Check spam/junk, verify your SMTP credentials and sender address, confirm environment variables are loaded, and watch server logs for email transport errors.

Career Services

Personalized career support to help you launch your tech career. Get résumé reviews, mock interviews, and industry insights—so you can showcase your new skills with confidence.