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:
UserAccountSessionVerificationToken
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.Texton long token fields - For SQLite, remove
@db.Textannotations
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_URLmust be your real HTTPS domain (g.,https://example.com) - Don’t commit
.envto git
5) Create a reusable Prisma client (recommended)
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
“I’m not receiving the magic link.”
Check:
- SMTP credentials are correct
EMAIL_FROMis 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_SECRETis 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_URLmatches 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.