Next Starter Logo
Deployment

Database Setup

How to configure PostgreSQL with Prisma 7 for Next Starter, including local Docker setup, running migrations, hosted database providers, and connection pooling for serverless deployments.

Overview

Next Starter uses PostgreSQL with Prisma 7 as the ORM. The Prisma client is generated into generated/prisma (not node_modules) and is gitignored. It regenerates as part of pnpm build, which runs prisma generate && prisma migrate deploy && next build.

Local PostgreSQL with Docker

The project includes a docker-compose.yml that starts a PostgreSQL 17 instance with the default credentials from .env.example.

docker-compose up -d

This creates a next_starter database accessible at localhost:5432.

Default credentials:

SettingValue
Hostlocalhost
Port5432
Userpostgres
Passwordpostgres
Databasenext_starter

The corresponding connection string:

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/next_starter"

Data persists in a named Docker volume (postgres_data). It survives container restarts but is removed when you run docker-compose down -v.

Prisma Configuration

Prisma 7 separates schema and datasource configuration. The database URL is not defined inside schema.prisma. It is configured in prisma.config.ts at the project root:

import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: env("DATABASE_URL"),
  },
});

DATABASE_URL must be set in your environment (or .env file) before running any Prisma commands.

Prisma Schema

The schema lives at prisma/schema.prisma. It defines five models used by Better Auth and the billing system:

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

datasource db {
  provider = "postgresql"
}

model User {
  id            String    @id
  name          String
  email         String    @unique
  emailVerified Boolean
  image         String?
  createdAt     DateTime
  updatedAt     DateTime
  role          String?
  banned        Boolean?
  banReason     String?
  banExpires    DateTime?
  accounts      Account[]
  sessions      Session[]

  settings Json?

  stripeCustomerId String?

  @@map("user")
}

model Session {
  id             String   @id
  expiresAt      DateTime
  token          String   @unique
  createdAt      DateTime
  updatedAt      DateTime
  ipAddress      String?
  userAgent      String?
  userId         String
  impersonatedBy String?
  user           User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@map("session")
}

model Account {
  id                    String    @id
  accountId             String
  providerId            String
  userId                String
  accessToken           String?
  refreshToken          String?
  idToken               String?
  accessTokenExpiresAt  DateTime?
  refreshTokenExpiresAt DateTime?
  scope                 String?
  password              String?
  createdAt             DateTime
  updatedAt             DateTime
  user                  User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@map("account")
}

model Verification {
  id         String    @id
  identifier String
  value      String
  expiresAt  DateTime
  createdAt  DateTime?
  updatedAt  DateTime?

  @@index([identifier])
  @@map("verification")
}

model Subscription {
  id                   String    @id
  plan                 String
  referenceId          String
  stripeCustomerId     String?
  stripeSubscriptionId String?
  status               String?   @default("incomplete")
  periodStart          DateTime?
  periodEnd            DateTime?
  trialStart           DateTime?
  trialEnd             DateTime?
  cancelAtPeriodEnd    Boolean?  @default(false)
  cancelAt             DateTime?
  canceledAt           DateTime?
  endedAt              DateTime?
  seats                Int?

  @@map("subscription")
}

Do not modify this schema to add Better Auth fields manually. Use the Better Auth CLI to generate schema additions when enabling new plugins, then run migrations.

Running Migrations

Development

pnpm dlx prisma migrate dev

This applies pending migrations, creates a new migration file if the schema has changed, and regenerates the Prisma client.

Production

pnpm dlx prisma migrate deploy

migrate deploy applies pending migrations without creating new ones and without prompting. pnpm build already runs prisma migrate deploy before building the app, so most deployment pipelines do not need to run this separately. It is safe to run on every deploy: it is a no-op when the database is already up to date.

Checking Migration Status

pnpm dlx prisma migrate status

Shows which migrations have been applied and which are pending.

Seeding Data

No seed file is included by default. To add one, create prisma/seed.ts and add a prisma.seed entry to package.json:

{
  "prisma": {
    "seed": "tsx prisma/seed.ts"
  }
}

Then run:

pnpm dlx prisma db seed

A typical seed file for development creates an admin user or inserts test subscription data.

Resetting the Database

To wipe all data and re-apply migrations from scratch (development only):

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

This removes the Docker volume (all data), starts a fresh PostgreSQL instance, and re-applies all migrations.

Hosted Database Providers

For production, use a managed PostgreSQL provider. All of the following work with Prisma by setting DATABASE_URL to the connection string they provide.

Neon

Neon offers serverless PostgreSQL with a generous free tier and built-in connection pooling, which works well in serverless environments.

  1. Create a project in the Neon dashboard.
  2. Copy the connection string from Connection Details.
  3. Set it as DATABASE_URL in your environment.

Neon connection strings include ?sslmode=require. Prisma handles this automatically.

Supabase

Supabase provides managed PostgreSQL with a dashboard, row-level security, and built-in storage. Next Starter only uses the PostgreSQL database, not Supabase Auth or Supabase Storage.

  1. Create a project in the Supabase dashboard.
  2. Go to Settings > Database and copy the connection string under Connection string > URI.
  3. Replace [YOUR-PASSWORD] with your database password.
  4. Set it as DATABASE_URL.

Railway

Railway provisions a PostgreSQL database alongside your app deployment in the same project. The connection string is available as an environment variable in the Railway dashboard.

  1. Add a PostgreSQL plugin to your Railway project.
  2. Copy DATABASE_URL from the plugin's Variables tab.
  3. Add it to your app service's environment variables.

Railway is a good choice for hosting the app container and database in one place.

Connection Pooling

For serverless deployments (Vercel, AWS Lambda), each function invocation may open a new database connection. PostgreSQL has a limited connection count, so high traffic can exhaust the pool.

Use a connection pooler in front of your database:

  • Neon: built-in pooling via the ?pgbouncer=true parameter in the connection string
  • Supabase: use the pooler connection string (port 6543) instead of the direct connection
  • PgBouncer: self-hosted pooler, available as a Railway plugin or run separately

When using a pooler, keep DATABASE_URL pointed at the pooled connection string for the application. For running migrations, you need a direct (non-pooled) connection because connection poolers in transaction mode are incompatible with Prisma migrations.

The .env.example documents a DIRECT_DATABASE_URL variable as a placeholder for this purpose. To use it, update prisma.config.ts to read from DIRECT_DATABASE_URL when running migrations, or temporarily set DATABASE_URL to the direct connection string before running prisma migrate deploy.

Example environment variables:

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

DIRECT_DATABASE_URL is not automatically wired into prisma.config.ts. It is provided in .env.example as a reference. To make Prisma use the direct URL for all migration commands, update prisma.config.ts:

import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: env("DIRECT_DATABASE_URL"),
  },
});

For long-running server deployments (Docker, Railway app service), connection pooling is less critical because the process stays alive and Prisma reuses connections.

On this page