Next Starter Logo
Deployment

Database Setup

How Next Starter connects PostgreSQL to Prisma 7: local Docker, migrations, managed providers, and connection pooling for serverless deployments.

How it works

Next Starter stores data in PostgreSQL and talks to it through Prisma 7. Prisma is an ORM: it lets you read and write the database with typed JavaScript (prisma.user.findMany()) instead of hand-written SQL.

Prisma 7 connects through a driver adapter: a small package that uses a normal Node Postgres driver to run queries. Next Starter uses @prisma/adapter-pg, which wraps node-postgres (the standard pg library). There is no separate Prisma query engine to ship or run.

The moving parts:

PieceFileWhat it does
Schema + generatorprisma/schema.prismaYour models and the prisma-client generator
Config + migrationsprisma.config.tsPoints Prisma at the schema and reads DATABASE_URL
Client instancelib/db.tsSets up the adapter and exports one shared client
Local databasedocker-compose.ymlPostgres 18 for development

Prisma turns your schema into a typed client. That client is generated into generated/prisma (not node_modules), and that folder is gitignored. Because the output is a custom folder, the prisma-client generator needs an explicit output path:

generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

pnpm build regenerates this folder. The build script runs prisma generate && next build && prisma migrate deploy, so a fresh deploy generates the client, builds the app, and then applies migrations.

Prisma 7 does not read the database URL from schema.prisma. The datasource block has no url. For CLI commands the URL comes from prisma.config.ts (env("DATABASE_URL")); for the running app it comes from the adapter in lib/db.ts. Either way, DATABASE_URL must be set before any prisma command runs.

The shared client

lib/db.ts builds a PrismaPg adapter from DATABASE_URL, hands it to PrismaClient, and stores one instance on globalThis. The global cache stops Next.js hot reloads from opening a new connection on every code change in development. Import the default export:

import prisma from "@/lib/db";

Local database

docker-compose.yml runs PostgreSQL 18 with the credentials already set in .env.example. Start it with:

docker-compose up -d
SettingValue
Host / Portlocalhost:5432
User / Passwordpostgres / postgres
Databasenext_starter
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/next_starter"

Data lives in the postgres_data volume and survives restarts. docker-compose down -v deletes the volume. To wipe everything and rebuild from scratch:

docker-compose down -v && docker-compose up -d && pnpm dlx prisma migrate dev

The models

The schema defines five models in prisma/schema.prisma. The first four back Better Auth (sign-in, sessions, accounts). The fifth backs billing.

ModelPurpose
UserAccounts, plus role/banned (admin), onboardingComplete, settings, stripeCustomerId
SessionActive sessions, including admin impersonation
AccountOAuth and password links for each user
VerificationEmail and token verification
SubscriptionStripe subscription state (plan, status, period, seats)

Don't hand-edit the auth models to add Better Auth fields. Enable the plugin, run the Better Auth CLI to write the schema changes, then migrate. See Authentication.

What you can change

Add your own model

A migration is a saved SQL change that brings your database in line with your schema. Prisma writes one each time you change models.

  1. Add the model to prisma/schema.prisma.

  2. Create and apply the migration:

    pnpm dlx prisma migrate dev --name add_post

    This writes a migration file to prisma/migrations/, runs it against your database, and regenerates generated/prisma.

  3. Query it through the shared client: prisma.post.findMany().

Point at a managed Postgres

For production, set DATABASE_URL to any managed Postgres connection string (Neon, Supabase, Railway, RDS, and so on). Nothing else changes, because the adapter speaks standard Postgres. Add sslmode=require if the provider needs TLS (most do). Apply your schema with prisma migrate deploy.

Key commands

CommandWhen to use it
pnpm dlx prisma migrate devDev: create and apply a migration, then regenerate the client
pnpm dlx prisma migrate deployProd: apply pending migrations only, with no prompts (also run by pnpm build)
pnpm dlx prisma migrate statusShow which migrations are applied vs. still pending
pnpm dlx prisma generateRegenerate generated/prisma after a schema change
pnpm dlx prisma studioBrowse and edit data in a local GUI

migrate deploy is safe to run on every deploy. It does nothing when the database is already up to date.

Seeding

No seed file ships by default. To add one, create prisma/seed.ts, register the command in prisma.config.ts, then run pnpm dlx prisma db seed. Prisma 7 reads the seed command from the config file, not from a prisma key in package.json:

migrations: {
  path: "prisma/migrations",
  seed: "pnpm dlx tsx prisma/seed.ts",
},

Connection pooling for serverless

On serverless platforms (Vercel, Lambda), each cold start can open a new database connection. Enough cold starts at once will hit Postgres's connection limit and cause errors. A pooler sits in front of the database and shares a small set of connections across many requests. Put one in front and point the app at the pooled URL:

  • Neon: append ?pgbouncer=true to the connection string
  • Supabase: use the pooler endpoint (port 6543)
  • PgBouncer: self-hosted, or the Railway plugin

Migrations need a direct, non-pooled connection. Transaction-mode poolers break prisma migrate. .env.example reserves DIRECT_DATABASE_URL for this:

DATABASE_URL="postgresql://user:pass@pooler-host:6543/db?pgbouncer=true&connection_limit=1"
DIRECT_DATABASE_URL="postgresql://user:pass@direct-host:5432/db"

DIRECT_DATABASE_URL is not wired up for you. To use it for migrations, point the datasource in prisma.config.ts at it:

datasource: {
  url: env("DIRECT_DATABASE_URL"),
},

For long-running deployments (Docker, a Railway service), pooling matters less. The process stays alive, so Prisma reuses its connections.

On this page