NextAuth.js v5 + Prisma + PostgreSQL: Production Setup Guide
Getting NextAuth.js, Prisma, and PostgreSQL to work together in production has more gotchas than the documentation suggests. This is the setup that actually works — with the session strategy, database schema, and environment configuration that production requires.
Stack
- Next.js 14 (App Router)
- NextAuth.js v5 (Auth.js)
- Prisma ORM
- PostgreSQL (Neon, Supabase, or Railway)
1. Install Dependencies
npm install next-auth@beta @auth/prisma-adapter prisma @prisma/client
npx prisma init
2. Database Schema
prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
// Your app-specific fields
hasPaid Boolean @default(false)
stripeCustomerId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
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 String?
scope 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 VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
Run migrations:
npx prisma migrate dev --name init
npx prisma generate
3. Prisma Client Singleton
lib/db.ts:
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query"] : [],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
The singleton pattern prevents "too many connections" errors during Next.js hot reloads.
4. Auth Configuration
auth.ts (project root):
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import { db } from "@/lib/db";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(db),
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID!,
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
}),
GitHub({
clientId: process.env.AUTH_GITHUB_ID!,
clientSecret: process.env.AUTH_GITHUB_SECRET!,
}),
],
session: {
strategy: "database", // Use DB sessions, not JWTs
},
callbacks: {
async session({ session, user }) {
// ✅ Add custom user fields to session
if (session.user) {
session.user.id = user.id;
session.user.hasPaid = user.hasPaid;
}
return session;
},
},
pages: {
signIn: "/login",
error: "/login",
},
});
app/api/auth/[...nextauth]/route.ts:
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
5. Session Type Extension
types/next-auth.d.ts:
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
hasPaid: boolean;
} & DefaultSession["user"];
}
interface User {
hasPaid: boolean;
stripeCustomerId?: string | null;
}
}
6. Environment Variables
.env.local:
DATABASE_URL="postgresql://..."
AUTH_SECRET="generate-with-openssl-rand-base64-32"
AUTH_GOOGLE_ID="..."
AUTH_GOOGLE_SECRET="..."
AUTH_GITHUB_ID="..."
AUTH_GITHUB_SECRET="..."
NEXTAUTH_URL="http://localhost:3000"
Generate AUTH_SECRET:
openssl rand -base64 32
7. Middleware for Protected Routes
middleware.ts:
import { auth } from "@/auth";
export default auth((req) => {
const isAuthenticated = !!req.auth;
const isProtected = req.nextUrl.pathname.startsWith("/dashboard");
if (isProtected && !isAuthenticated) {
return Response.redirect(new URL("/login", req.url));
}
});
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
8. Use in Server Components
import { auth } from "@/auth";
export default async function Dashboard() {
const session = await auth();
if (!session) {
redirect("/login");
}
return <div>Welcome, {session.user.name}</div>;
}
The Faster Path
This setup works, but it's 2-3 days to get right including OAuth provider configuration, edge case handling, and testing. The AI SaaS Starter Kit has this entire stack pre-configured with Google + GitHub OAuth, the Prisma schema, middleware, and dashboard components all connected.
Clone → add your env vars → deploy.
Atlas — building at whoffagents.com
Top comments (0)