Skip to content

Database

PostgreSQL via Prisma ORM. Schema lives at prisma/schema.prisma; the generated client is output to src/generated/prisma (gitignored, regenerated by bun run prisma:generate).

Models

Better Auth-managed tables (comment in the schema says "do not modify" - Better Auth owns their shape):

  • Session - id, expiresAt, token (unique), ipAddress?, userAgent?, timestamps, belongs to User (cascade delete).
  • Account - id, accountId, providerId, OAuth/credential fields (accessToken, refreshToken, idToken, expiry timestamps, scope, password), belongs to User (cascade delete).
  • Verification - id, identifier, value, expiresAt, timestamps.

Application user (also managed by Better Auth, via its Prisma adapter):

  • User - id, name, email (unique), emailVerified, image?, timestamps. Relations: sessions[], accounts[], posts[].

Example domain model (comment says "replace with your own" - this is the one meant to be deleted/extended):

  • Post - id (cuid), title, content, published (default false), timestamps, belongs to User (cascade delete, indexed on userId). Backs the demo/posts feature - see features.md.

Migration workflow

bash
bun run prisma:migrate         # create + apply a migration in dev (interactive)
bun run prisma:generate        # regenerate the TypeScript client after schema changes
bun run prisma:migrate-deploy  # apply pending migrations non-interactively (used in prod)
bun run prisma:studio          # open Prisma Studio, a GUI for browsing/editing data

Always run prisma:generate after prisma:migrate (or after pulling schema changes from git) - the generated client is what src/lib/db.ts imports, and it goes stale as soon as the schema changes.

In production, bun run prisma:migrate-deploy runs automatically on every container start - see docker-entrypoint.sh and deployment.md.

The Prisma client singleton

src/lib/db.ts exports a single cached PrismaClient instance, cached on global in development. Next.js's dev server hot-reloads modules on every file save; without this cache, every reload would create a fresh PrismaClient (and a fresh connection pool) without closing the old one, quickly exhausting Postgres's connection limit. Production doesn't hot-reload, so no caching workaround is needed there - every feature router imports the same singleton via import prisma from "@/lib/db".