Skip to content

ADR-0012: Prisma Database Package Configuration

Last Updated: 2026-02-06 Status: Active Context: Decksmith


Context

Decksmith needs type-safe database access for its PostgreSQL database hosted on Supabase. The packages/db package must provide:

  • A Prisma schema defining all database models
  • A type-safe client for querying the database
  • A migration workflow for versioned schema changes
  • Integration with the monorepo's shared environment configuration

Prisma 7 introduced breaking changes from v6, notably removing the url field from the datasource block in schema.prisma in favor of a new prisma.config.ts configuration file.

Current Decision

Prisma 7 Configuration Pattern

We use Prisma 7's prisma.config.ts for datasource configuration instead of inline url in schema.prisma:

prisma/schema.prisma — no URL:

prisma
datasource db {
  provider = "postgresql"
}

prisma.config.ts — URL loaded from environment:

typescript
import path from 'node:path';
import { config } from 'dotenv';
import { defineConfig, env } from 'prisma/config';

config({ path: path.resolve(import.meta.dirname, '../../.env') });

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

Singleton Client Pattern

We use a singleton pattern with optional typing to prevent multiple connection pools:

typescript
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env['NODE_ENV'] !== 'production') {
  globalForPrisma.prisma = prisma;
}

Key details:

  • prisma? (optional) is more type-honest than a non-optional assertion — at first call, globalThis.prisma is undefined
  • ?? (nullish coalescing) instead of || — only checks for null/undefined, not other falsy values
  • process.env['NODE_ENV'] — bracket notation required by TypeScript strict mode (noPropertyAccessFromIndexSignature)

Global Environment File

A single .env file at the monorepo root contains DATABASE_URL. The prisma.config.ts loads it via dotenv with a relative path to the root. This avoids duplicating .env files per package.

Migration Workflow

CommandPurposeWhen to use
db:pushSync schema to database without historyEarly prototyping
db:migrate:devCreate versioned migration filesDevelopment
db:migrate:deployApply migrations without creating new onesProduction
db:generateGenerate TypeScript client from schemaAfter schema changes

Rule: Once the first migration is created, always use db:migrate:dev for schema changes.

Generated Client in CI

The Prisma client is generated (lives in node_modules/.prisma/client/), not committed. CI must run prisma generate before lint and typecheck. Since generate only reads the schema and does not connect to the database, a dummy DATABASE_URL is sufficient:

yaml
- name: Generate Prisma client
  run: pnpm --filter @decksmith/db db:generate
  env:
    DATABASE_URL: 'postgresql://localhost:5432/dummy'

Rationale

Why Prisma 7 Config Pattern

  • Separation of concerns: Schema defines structure, config handles runtime environment
  • Security: No hardcoded URLs in schema files that could be accidentally committed
  • Flexibility: Config file can load environment variables from any source

Why Singleton Pattern

  • Hot reload safety: In development, module re-evaluation would create new PrismaClient instances, each opening a new connection pool
  • Serverless safety: In serverless environments, warm containers reuse the global instance instead of creating new connections per invocation
  • Production skip: In production, the global assignment is skipped — each process gets exactly one client instance without the overhead of checking globalThis

Why Global .env

  • Single source of truth: One file for all packages, no drift between configurations
  • Monorepo convention: Root .env is already in .gitignore, no additional configuration needed
  • Simplicity: dotenv resolves path relative to monorepo root via import.meta.dirname

Why dotenv in pnpm Catalog

dotenv is added to the pnpm catalog because multiple packages will need environment variable loading (e.g., apps/api, apps/worker). Centralizing the version avoids drift.

Trade-offs

Benefits:

  • Type-safe database access with auto-generated TypeScript types
  • Versioned migrations committed alongside code
  • Single environment configuration for the entire monorepo
  • CI-safe with dummy URL for type generation

Costs:

  • prisma generate must run before lint/typecheck (extra CI step)
  • dotenv dependency for config loading
  • Singleton pattern requires understanding of globalThis and type assertions

Risks:

  • Prisma 7 is relatively new — some ecosystem tools may not yet support the config pattern
    • Mitigation: Pin to ^7.3.0, monitor Prisma releases
  • Generated client not committed — developer must run db:generate after cloning
    • Mitigation: Document in onboarding, add to pnpm install postinstall if needed

Evolution History

2026-02-06: Initial decision

  • Adopted Prisma 7 with prisma.config.ts pattern
  • Implemented singleton client with optional typing and nullish coalescing
  • Established global .env at monorepo root loaded via dotenv
  • Defined migration workflow: push for prototyping, migrate for versioning
  • Added prisma generate step to CI workflow with dummy DATABASE_URL

References

Built with VitePress