← Back to blog
·6 min read·By Utkarsh Singh

How to Set Up Prisma with PostgreSQL in Next.js (Supabase, Neon & Railway)

A complete guide to setting up Prisma ORM with PostgreSQL in a Next.js App Router project — provider-agnostic setup, migrations, seed scripts, and production-safe deployment.

nextjsprismapostgresqldatabasesupabaseneon

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:


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:

  1. Compares your schema to the current database state
  2. Generates a SQL migration file in prisma/migrations/
  3. Applies the migration to your development database
  4. 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:

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.

Skip the setup. Ship your product.

ZeroDrag gives you auth, payments, database, email, and AI — pre-wired. Starter at $119, Pro at $169. One-time payment.

Get ZeroDrag →