How to Set Up Prisma with PostgreSQL in Next.js (Supabase, Neon & Railway)
PostgreSQL + Prisma is the standard database setup for production Next.js SaaS apps in 2026. Prisma gives you type-safe queries, a clean migration workflow, and a schema-first approach that keeps your database and TypeScript types in sync automatically.
This guide covers a complete, production-ready Prisma setup — provider-agnostic so it works with Supabase, Neon, Railway, AWS RDS, or any other PostgreSQL host.
Choosing a PostgreSQL Provider
Before touching any code, you need a database. Here's the honest comparison:
Neon — Serverless PostgreSQL with a generous free tier and automatic connection pooling. Best for new projects and indie hackers. Scales well and the developer experience is excellent.
Supabase — PostgreSQL with extras (auth, storage, realtime). Great if you want a batteries-included backend. Be aware that Supabase's connection pooling behaves differently from raw PostgreSQL.
Railway — Simple, predictable pricing and easy deployment. Good for projects that want a straightforward managed Postgres without the extras.
AWS RDS / Google Cloud SQL — Best for production apps with strict compliance or infrastructure requirements. More setup, more control.
For a new SaaS project, start with Neon or Supabase. You can migrate later — Prisma's provider-agnostic setup makes this straightforward.
Step 1: Install Prisma
npm install prisma @prisma/client
npx prisma init
This creates:
prisma/schema.prisma— your database schema.env— with aDATABASE_URLplaceholder
Step 2: Configure the Database URL
In your .env:
# Neon
DATABASE_URL="postgresql://username:password@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require"
# Supabase
DATABASE_URL="postgresql://postgres:password@db.xxx.supabase.co:5432/postgres"
# Railway
DATABASE_URL="postgresql://postgres:password@containers-us-west-xxx.railway.app:6543/railway"
For Supabase and Neon, also set a DIRECT_URL for migrations:
# Required for Supabase and Neon with connection pooling
DIRECT_URL="postgresql://your-direct-connection-string"
Step 3: Set Up the Prisma Schema
Replace the contents of prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL") // Required for Supabase/Neon connection pooling
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Auth
accounts Account[]
sessions Session[]
// Billing
stripeCustomerId String? @unique
stripeSubscriptionId String? @unique
razorpaySubscriptionId String? @unique
stripePriceId String?
stripeCurrentPeriodEnd DateTime?
plan String @default("free")
@@map("users")
}
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])
@@map("accounts")
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model VerificationToken {
identifier String
token String
expires DateTime
@@unique([identifier, token])
@@map("verification_tokens")
}
Step 4: Create the Prisma Client Singleton
Create 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: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
This singleton pattern prevents creating multiple Prisma Client instances during hot reloads in development — a common source of "too many connections" errors.
Step 5: Run Your First Migration
npx prisma migrate dev --name init
This:
- Compares your schema to the current database state
- Generates a SQL migration file in
prisma/migrations/ - Applies the migration to your development database
- Regenerates the Prisma Client
In production, use:
npx prisma migrate deploy
Never run migrate dev in production. It prompts for confirmations and can reset data.
Step 6: Write a Seed Script
Create prisma/seed.ts:
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
// Create a test user
const user = await prisma.user.upsert({
where: { email: "test@example.com" },
update: {},
create: {
email: "test@example.com",
name: "Test User",
plan: "pro",
},
});
console.log("Seeded user:", user.email);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Add to package.json:
{
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
}
}
Run with:
npx prisma db seed
Use upsert in seed scripts, not create. This makes seeds idempotent — you can run them multiple times without errors.
Step 7: Using Prisma in Server Components and API Routes
In a Server Component:
import { prisma } from "@/lib/prisma";
import { auth } from "@/auth";
export default async function DashboardPage() {
const session = await auth();
const user = await prisma.user.findUnique({
where: { id: session?.user?.id },
select: {
name: true,
email: true,
plan: true,
},
});
return <div>Plan: {user?.plan}</div>;
}
In an API route:
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { auth } from "@/auth";
export async function GET(req: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const user = await prisma.user.findUnique({
where: { id: session.user.id },
});
return NextResponse.json(user);
}
Common Gotchas
Connection pooling in serverless environments. Serverless functions (Vercel, etc.) can open many simultaneous database connections. Use Neon's connection pooling URL or Supabase's pooler URL as your DATABASE_URL, and the direct URL only for migrations via DIRECT_URL.
Forgetting to run prisma generate after schema changes. After editing schema.prisma, run npx prisma generate to update the TypeScript types. This happens automatically with migrate dev but not if you manually edit the schema without migrating.
Using migrate dev in CI/CD. Use migrate deploy in your deployment pipeline, not migrate dev. The latter is interactive and designed for local development only.
Not adding prisma/migrations to version control. Migration files should be committed. They're the record of how your schema evolved and are needed for migrate deploy to work correctly.
Production Deployment Checklist
Before going live:
- [ ]
DATABASE_URLandDIRECT_URLset in production environment - [ ]
npx prisma migrate deployruns as part of your deployment pipeline - [ ] Connection pooler URL used for
DATABASE_URL(not direct connection) - [ ]
npx prisma generateruns before build ("postinstall": "prisma generate"in package.json) - [ ] SSL mode enabled in connection string (
?sslmode=require)
Add to package.json to ensure the client is always generated before build:
{
"scripts": {
"postinstall": "prisma generate",
"build": "prisma migrate deploy && next build"
}
}
The Time Investment
A clean Prisma + PostgreSQL setup done properly takes about 4–5 hours — choosing a provider, configuring connection strings correctly, writing the schema, setting up migrations, and testing the seed script. Add another hour if you're debugging connection pooling issues with Supabase or Neon.
ZeroDrag ships with this entire setup pre-configured and tested across Supabase, Neon, Railway, AWS RDS, and Google Cloud SQL. The schema includes auth tables, billing fields, and seed scripts — ready to run on first clone.
Written by Utkarsh Singh. Last updated March 2026.