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:
| Piece | File | What it does |
|---|---|---|
| Schema + generator | prisma/schema.prisma | Your models and the prisma-client generator |
| Config + migrations | prisma.config.ts | Points Prisma at the schema and reads DATABASE_URL |
| Client instance | lib/db.ts | Sets up the adapter and exports one shared client |
| Local database | docker-compose.yml | Postgres 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| Setting | Value |
|---|---|
| Host / Port | localhost:5432 |
| User / Password | postgres / postgres |
| Database | next_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 devThe models
The schema defines five models in prisma/schema.prisma. The first four back Better Auth (sign-in, sessions, accounts). The fifth backs billing.
| Model | Purpose |
|---|---|
User | Accounts, plus role/banned (admin), onboardingComplete, settings, stripeCustomerId |
Session | Active sessions, including admin impersonation |
Account | OAuth and password links for each user |
Verification | Email and token verification |
Subscription | Stripe 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.
-
Add the
modeltoprisma/schema.prisma. -
Create and apply the migration:
pnpm dlx prisma migrate dev --name add_postThis writes a migration file to
prisma/migrations/, runs it against your database, and regeneratesgenerated/prisma. -
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
| Command | When to use it |
|---|---|
pnpm dlx prisma migrate dev | Dev: create and apply a migration, then regenerate the client |
pnpm dlx prisma migrate deploy | Prod: apply pending migrations only, with no prompts (also run by pnpm build) |
pnpm dlx prisma migrate status | Show which migrations are applied vs. still pending |
pnpm dlx prisma generate | Regenerate generated/prisma after a schema change |
pnpm dlx prisma studio | Browse 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=trueto 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.
Cloudflare Turnstile
Next Starter uses Cloudflare Turnstile (MANAGED mode) to block bots on the contact form, with a server-side token check and almost no friction for real users.
Deploy to Vercel
Deploy Next Starter to Vercel: import the repo, set environment variables, run build-time migrations, and wire up the domain, Stripe webhook, and Turnstile.