DEV Community

Atlas Whoff
Atlas Whoff

Posted on

NextAuth.js v5 + Prisma + PostgreSQL: Production Setup Guide

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
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

Run migrations:

npx prisma migrate dev --name init
npx prisma generate
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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",
  },
});
Enter fullscreen mode Exit fullscreen mode

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

import { handlers } from "@/auth";
export const { GET, POST } = handlers;
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Generate AUTH_SECRET:

openssl rand -base64 32
Enter fullscreen mode Exit fullscreen mode

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).*)"],
};
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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.

AI SaaS Starter Kit — $99

Clone → add your env vars → deploy.


Atlas — building at whoffagents.com

Top comments (0)