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 reaches it through Prisma 7, which ships no query engine binary. Queries run through a driver adapter instead, and this app uses @prisma/adapter-pg on top of node-postgres.
| 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 generates the typed client into generated/prisma rather than node_modules, and that folder is gitignored. Because the output is a custom folder, the generator needs an explicit output path:
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
}Note the missing url on that datasource. Prisma 7 doesn't read the database URL from schema.prisma at all. CLI commands take it from prisma.config.ts (env("DATABASE_URL")), and the running app takes it from the adapter in lib/db.ts. Either way, DATABASE_URL has to be set before any prisma command runs. That config file opens with import "dotenv/config", which is what lets the CLI see your local .env.
lib/db.ts builds a PrismaPg adapter from DATABASE_URL, hands it to PrismaClient, and caches the instance on globalThis so a hot reload in development doesn't open a fresh connection every time you save a file. Import the default export:
import prisma from "@/lib/db";Get Started covers getting Postgres running on your machine.
The models
prisma/schema.prisma defines six models. Four back Better Auth core, Subscription backs billing, and Apikey backs the API key plugin.
| 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 (providerId, accountId, tokens, hashed password) |
Verification | Email and token verification |
Subscription | Stripe subscription state (plan, status, period, seats) |
Apikey | Per-user API keys (hashed key, referenceId = user ID, rate limit and expiry fields) |
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
Add the model to prisma/schema.prisma, then run both commands:
pnpm prisma migrate dev --name add_post
pnpm prisma generateThe first writes a migration file into prisma/migrations/ and applies it to your database. The second is not optional. migrate dev doesn't regenerate the client, so skip it and prisma.post won't exist on the typed client yet.
Build your first SaaS app walks a model through to a working feature.
Point at a managed Postgres
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 wants TLS, which most do, then apply your schema with prisma migrate deploy.
Commands
| Command | When to use it |
|---|---|
pnpm prisma migrate dev | Dev: create and apply a migration. Follow it with generate |
pnpm prisma migrate deploy | Prod: apply pending migrations, no prompts (also the last step of pnpm build) |
pnpm prisma migrate status | Show which migrations are applied and which are still pending |
pnpm prisma generate | Regenerate generated/prisma. migrate dev doesn't run it; pnpm build does |
pnpm 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. Create prisma/seed.ts, register the command in prisma.config.ts, then run pnpm prisma db seed. The CLI reads the seed command from the migrations block of that config file:
migrations: {
path: "prisma/migrations",
seed: "pnpm dlx tsx prisma/seed.ts",
},Connection pooling for serverless
On Vercel or Lambda, every cold start can open its own database connection, and enough of them at once will exhaust the Postgres connection limit. Put a pooler in front and point DATABASE_URL at the pooled connection string. Neon and Supabase both provide one; PgBouncer is the self-hosted route.
Migrations want a direct, non-pooled connection, because transaction-mode pooling drops the session state some migration steps rely on. The template has no variable for that string. prisma.config.ts reads DATABASE_URL and nothing else, so the build-time migrate deploy goes through whatever URL you gave the app. To split the two, add a variable such as DIRECT_DATABASE_URL to your environment and point the prisma.config.ts datasource at it. That swaps the URL for every Prisma CLI command, migrations included, and leaves the running app on DATABASE_URL since lib/db.ts reads that separately:
datasource: {
url: env("DIRECT_DATABASE_URL"),
},A ?connection_limit= parameter on the URL does nothing here. The pool belongs to pg, so you size it by passing a max alongside connectionString in lib/db.ts.
For long-running deployments (Docker, a Railway service) pooling matters less. The process stays alive, so Prisma reuses its connections.
Cloudflare Turnstile
Next Starter puts a Cloudflare Turnstile check (MANAGED mode) on the contact form, verified server-side and invisible for most visitors.
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.