Production-Grade Auth with NextAuth.js
A practical walkthrough of setting up credentials + OAuth authentication with NextAuth v5, Prisma, and Postgres — including session management, role-based access, and security defaults.
Auth is not a feature
Every project we ship needs authentication. Rather than rebuilding it each time, we have a standard auth layer built on NextAuth.js v5 (Auth.js) that we drop into every project. Here's what it includes.
The stack
- NextAuth.js v5 — the framework-agnostic auth library
- Prisma adapter — sessions and accounts stored in Postgres
- Credentials provider — email + password with bcrypt
- OAuth providers — GitHub and Google as optional extras
- Role-based access — user and admin roles on the session
Key decisions
Credentials + OAuth, not just OAuth
Many tutorials show OAuth-only setups. In practice, our clients want email/password as the primary flow with OAuth as an option. NextAuth's credentials provider works well here — just make sure you're hashing passwords with bcrypt (not argon2, not scrypt) for maximum compatibility.
Database sessions, not JWT
JWTs are stateless and fast, but you can't revoke them. For a platform where users manage projects and settings, database sessions let us invalidate all sessions on password change and give admins the ability to force-logout users. The latency cost of a database read per request is negligible.
The session callback
We extend the session with the user's role and id:
callbacks: {
session({ session, user }) {
session.user.id = user.id;
session.user.role = user.role;
return session;
},
},This lets any server component or API route check session.user.role === "admin" without a database call.
Security defaults
- Rate limiting on login routes (5 attempts per minute per IP)
- bcrypt cost factor of 12
- Session expiry at 7 days with sliding window
- Admin routes check role in middleware, not just in the page
The middleware pattern
export { auth as middleware } from class="tok-string">"@/lib/auth/config";
export const config = {
matcher: [class="tok-string">"/dashboard/:path*", class="tok-string">"/api/admin/:path*"],
};This redirects unauthenticated users to login and checks admin role for admin routes. One file, zero config per route.