Datenbank-Übersicht

Supabase PostgreSQL mit Prisma ORM — Architektur, Verbindungskonfiguration und Datenzugriffsmuster

Kit verwendet Supabase als gehostete PostgreSQL-Datenbank und Prisma als ORM. Diese Kombination bietet dir typsicheren Datenbankzugriff in TypeScript, automatisches Migrations-Management und Zugang zu PostgreSQL-Erweiterungen wie pgvector für KI-gestützte semantische Suche.
Diese Seite behandelt die Architektur und Datenzugriffsmuster. Für die vollständige Schema-Referenz, siehe Schema & Modelle. Für Migrations-Workflows, siehe Migrationen & Seeding.

Architektur

Die Datenbankschicht verbindet deine Next.js-Anwendung über drei Ebenen mit PostgreSQL:
Next.js Application
    |
    |--- Server Components ──> Repository / Query Layer ──> Prisma Client
    |--- API Routes ─────────> Repository / Query Layer ──> Prisma Client
    |--- Server Actions ─────> Repository / Query Layer ──> Prisma Client
    |
    v
Prisma Client (Singleton)
    |
    |--- Queries ──> Pooled Connection (pgbouncer) ──> Supabase PostgreSQL
    |--- Migrations ──> Direct Connection ───────────> Supabase PostgreSQL
    |
    v
PostgreSQL (Supabase)
    |--- pgvector extension (semantic search)
    |--- 10 tables (users, subscriptions, AI, credits, files, email)
Warum sowohl Supabase als auch Prisma? Jedes Tool übernimmt das, was es am besten kann:
  • Prisma stellt die typsichere Query-API, das Schema-Management und die Migrations-Toolchain bereit. Jede Datenbankabfrage im Anwendungscode läuft über Prisma und bietet dir vollständige TypeScript-Autovervollständigung und Typsicherheit zur Kompilierzeit.
  • Supabase stellt die gehostete PostgreSQL-Instanz, Connection-Pooling via pgbouncer, das Dashboard zur Datenbankinspektion und PostgreSQL-Erweiterungen wie pgvector bereit. Du interagierst nie direkt mit dem Supabase JS-Client für Datenabfragen — Prisma übernimmt das alles.

Verbindungskonfiguration {#connection-configuration}

Supabase benötigt zwei Verbindungs-URLs — eine gepoolte (für Anwendungsabfragen) und eine direkte (für Schema-Migrationen). Diese wird im Prisma-Schema konfiguriert:
prisma/schema.prisma
generator client {
  provider        = "prisma-client-js"
  binaryTargets   = ["native", "rhel-openssl-3.0.x"]
  previewFeatures = ["postgresqlExtensions"]
}

datasource db {
  provider   = "postgresql"
  // Use pooled connection for queries (with pgbouncer)
  url        = env("DATABASE_URL")
  // Use direct connection for migrations (without pgbouncer)
  directUrl  = env("DIRECT_URL")
  extensions = [vector]
}
Die entsprechenden Umgebungsvariablen in apps/boilerplate/.env.local:
bash
# Gepoolte Verbindung (für Abfragen) — Port 6543 bei Supabase
DATABASE_URL="postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true"

# Direkte Verbindung (für Migrationen) — Port 5432 bei Supabase
DIRECT_URL="postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:5432/postgres"
Beide URLs findest du im Supabase-Projektdashboard unter Settings > Database > Connection string. Wähle das Format "URI" und kopiere die gepoolte Variante (Port 6543) sowie die direkte Variante (Port 5432).

Prisma Client

Der Prisma-Client wird als Singleton initialisiert, um bei Next.js Hot Reloads in der Entwicklung keine Verbindungserschöpfung zu verursachen. Er behandelt Test- und Demo-Umgebungen auch automatisch:
src/lib/db/prisma.ts — Singleton Pattern
const prismaClientSingleton = () => {
  // Check for test/demo environment
  // Demo mode uses mock data, no real database needed
  const isDemoMode = process.env.NEXT_PUBLIC_DEMO_MODE === 'true'
  const isClerkDisabled = process.env.NEXT_PUBLIC_CLERK_ENABLED === 'false'
  const isTestEnvironment = isDemoMode || isClerkDisabled

  // In test/demo environment, use mock client to avoid database connection
  if (isTestEnvironment) {
    console.log('[TEST/DEMO] Using mock Prisma client - no database connection')
    return createMockPrismaClient()
  }

  return new PrismaClient({
    // Optimize for serverless/edge environments
    log:
      process.env.NODE_ENV === 'development'
        ? ['query', 'error', 'warn']
        : ['error'],
    // Error formatting
    errorFormat: process.env.NODE_ENV === 'development' ? 'pretty' : 'minimal',
  })
}

// Prevent multiple instances during development hot reload
export const prisma = globalForPrisma.prisma ?? prismaClientSingleton()

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}
Drei wichtige Punkte:
  1. Hot-Reload-Schutz — In der Entwicklung wird der Client auf globalThis gespeichert, sodass Hot Module Replacement bei jeder Dateiänderung keine neuen Datenbankverbindungen erstellt.
  2. Test-/Demo-Modus — Wenn NEXT_PUBLIC_DEMO_MODE=true oder NEXT_PUBLIC_CLERK_ENABLED=false gesetzt ist, wird stattdessen ein Mock-Proxy-Client zurückgegeben. Dadurch kann die gesamte Anwendung ohne Datenbankverbindung für E2E-Tests und Demos betrieben werden.
  3. Graceful Shutdown — In der Produktion trennt sich der Client beim Beenden des Prozesses sauber, um Verbindungslecks zu verhindern.
Importiere den Client überall in deinem serverseitigen Code:
typescript
import { prisma } from '@/lib/db/prisma'

// Verwendung in Server Components, API-Routen oder Server Actions
const user = await prisma.user.findUnique({
  where: { clerkId: userId }
})

Datenzugriffsmuster

Kit organisiert den Datenbankzugriff in drei Ebenen, die jeweils einem anderen Zweck dienen:

1. Repository Pattern

Die Repository-Ebene bietet eine Abstraktion über Prisma mit automatischer Test-Modus-Erkennung. Jedes Repository erweitert RepositoryBase, das in Testumgebungen Mock-Daten und in der Produktion echte Prisma-Abfragen zurückgibt:
src/lib/db/repository-base.ts
export abstract class RepositoryBase<T> {
  /**
   * Detects if application is running in test mode
   *
   * Test mode is detected by:
   * - NODE_ENV === 'test' (but overridden by DISABLE_REPOSITORY_MOCKS)
   * - NEXT_PUBLIC_CLERK_ENABLED === 'false' (E2E test environment)
   *
   * CRITICAL: Unit tests set DISABLE_REPOSITORY_MOCKS=true to use Prisma mocks.
   * This allows unit tests to have full control over database behavior while
   * E2E tests use Repository mock data (no database required).
   *
   * @returns true if in test mode, false for production/development/unit-tests
   */
  protected isTestMode(): boolean {
    // CRITICAL: Allow unit tests to disable repository mocking
    // Unit tests mock Prisma directly and need full control over data flow
    if (process.env.DISABLE_REPOSITORY_MOCKS === 'true') {
      return false
    }

    // E2E tests and other test scenarios use repository mocks
    return (
      process.env.NODE_ENV === 'test' ||
      process.env.NEXT_PUBLIC_CLERK_ENABLED === 'false'
    )
  }

  /**
   * Returns mock data for test environment
   *
   * Each repository must implement this method to provide
   * realistic test data that matches production schema.
   *
   * @returns Mock data instance
   */
  protected abstract getMockData(): T

  /**
   * Optional: Get mock data with custom properties
   *
   * Allows repositories to provide flexible mock data generation
   *
   * @param overrides - Partial properties to override defaults
   * @returns Mock data with overrides applied
   */
  protected getMockDataWithOverrides(overrides?: Partial<T>): T {
    return {
      ...this.getMockData(),
      ...overrides,
    } as T
  }
}
Verwende Repositories für Leseoperationen, die nahtlos in Produktions- und Testumgebungen funktionieren müssen. Das userRepository ist das primäre Beispiel:
typescript
import { userRepository } from '@/lib/db/repositories'

// Funktioniert in der Produktion (echte Abfragen) und in Tests (Mock-Daten) — keine Codeänderungen nötig
const user = await userRepository.findByClerkId(clerkId)
const userWithSub = await userRepository.findByClerkIdWithSubscription(clerkId)

2. Query-Module

Query-Module sind schlanke, fokussierte Funktionen, die Prisma-Operationen für bestimmte Modelle kapseln. Sie bieten eine saubere API ohne die Test-Modus-Abstraktion:
src/lib/db/queries/users.ts
import { prisma } from '@/lib/db/prisma'
import { userRepository } from '@/lib/db/repositories'
import type { User } from '@prisma/client'

/**
 * Get user by Clerk ID
 */
export async function getUserByClerkId(clerkId: string): Promise<User | null> {
  return userRepository.findByClerkId(clerkId)
}

/**
 * Get user by ID
 */
export async function getUserById(id: string): Promise<User | null> {
  return userRepository.findById(id)
}

/**
 * Create user from Clerk data
 */
export async function createUser(clerkId: string): Promise<User> {
  return prisma.user.create({
    data: {
      clerkId,
    },
  })
}

/**
 * Update user
 */
export async function updateUser(
  id: string,
  data: Partial<User>
): Promise<User> {
  return prisma.user.update({
    where: { id },
    data,
  })
}

/**
 * Delete user
 */
export async function deleteUser(id: string): Promise<User> {
  return prisma.user.delete({
    where: { id },
  })
}

/**
 * Get user bonus credit preference
 */
export async function getUserBonusPreference(
  userId: string
): Promise<{ bonusCreditsAutoUse: boolean } | null> {
  return prisma.user.findUnique({
    where: { id: userId },
    select: { bonusCreditsAutoUse: true },
  })
}

/**
 * Update user bonus credit preference
 */
export async function updateUserBonusPreference(
  userId: string,
  bonusCreditsAutoUse: boolean
): Promise<{ bonusCreditsAutoUse: boolean }> {
  return prisma.user.update({
    where: { id: userId },
    data: { bonusCreditsAutoUse },
    select: { bonusCreditsAutoUse: true },
  })
}
Das Verzeichnis apps/boilerplate/src/lib/db/queries/ enthält Module für jede Domain: users.ts, subscriptions.ts, files.ts und email-logs.ts. Diese Funktionen rufst du aus API-Routen und Server Actions auf.

3. Direkter Prisma-Zugriff

Für komplexe Operationen, die Transaktionen, Raw SQL oder operationen über mehrere Modelle hinweg erfordern, verwende den Prisma-Client direkt. Dies ist in Services und Managern üblich:
typescript
import { prisma } from '@/lib/db/prisma'

// Atomare Credit-Abbuchung mit Row-Level Locking
const result = await prisma.$transaction(async (tx) => {
  const user = await tx.$queryRaw`
    SELECT "creditBalance" FROM "User"
    WHERE "id" = ${userId} FOR UPDATE
  `
  await tx.user.update({
    where: { id: userId },
    data: { creditBalance: { decrement: cost } }
  })
  await tx.creditTransaction.create({
    data: { userId, amount: -cost, type: 'usage' }
  })
})

Best Practice für Server Components

In Server Components solltest du die Datenbank immer direkt abfragen, anstatt deine eigenen API-Routen aufzurufen:
typescript
// src/app/(dashboard)/layout.tsx — Server Component

// RICHTIG: Direkte Datenbankabfrage (< 100ms)
const user = await userRepository.findByClerkId(clerkId)

// FALSCH: Self-referencing fetch — Server blockiert und wartet auf sich selbst (7+ Sekunden!)
const res = await fetch('http://localhost:3000/api/users/me')
Kit folgt diesem Muster im gesamten Dashboard-Layout und auf allen serverseitig gerenderten Seiten.

Modell-Übersicht

Kit wird mit 10 Prisma-Modellen ausgeliefert, die die wichtigsten SaaS-Features abdecken. Hier eine Zusammenfassung — für vollständige Felddetails, siehe Schema & Modelle.
ModellZweckWichtige Relationen
UserZentrales Benutzer-Entity mit Clerk-Sync, unterstützt duales Preismodell (Credits + klassisches SaaS)Hat viele: Subscription, File, EmailLog, AIUsage, AIConversation, CreditTransaction, BonusCreditPurchase
SubscriptionLemon Squeezy Subscription-Tracking mit Plan-VerlaufGehört zu User (Cascade Delete)
CreditTransactionUnveränderliches Credit-Ledger (Nutzung, Rückerstattungen, Resets, Käufe)Gehört zu User
BonusCreditPurchaseTop-up-Credit-Käufe über Lemon SqueezyGehört zu User
AIUsageKI-Token- und Kosten-Tracking pro AnfrageGehört zu User
AIConversationChat-Konversations-Threads mit Session-TrackingGehört zu User, hat viele AIMessage
AIMessageEinzelne Chat-Nachrichten (User, Assistant, System)Gehört zu AIConversation
FAQChunkRAG-Wissensbasis mit pgvector-Embeddings (1536 Dimensionen)Eigenständig
FileVercel Blob Datei-Upload-MetadatenGehört zu User
EmailLogResend E-Mail-Zustellungs-Tracking mit AnalyticsGehört zu User

Umgebungsvariablen

VariableErforderlichZweck
DATABASE_URLJaGepoolte PostgreSQL-Verbindung (pgbouncer) für Anwendungsabfragen
DIRECT_URLJaDirekte PostgreSQL-Verbindung für Prisma-Migrationen
NEXT_PUBLIC_SUPABASE_URLJaSupabase-Projekt-URL (für Storage und andere Supabase-Features)
NEXT_PUBLIC_SUPABASE_ANON_KEYJaSupabase Anonymous Key (öffentlich, sicher für Client-Seite)
SUPABASE_SERVICE_ROLE_KEYNeinSupabase Service Role Key (nur Server, umgeht RLS)

Wichtige Dateien

DateiZweck
apps/boilerplate/prisma/schema.prismaDatenbankschema — alle Modelle, Relationen, Indexes und Konfiguration
apps/boilerplate/src/lib/db/prisma.tsPrisma-Client-Singleton mit Test-Modus und Graceful Shutdown
apps/boilerplate/src/lib/db/client.tsAbwärtskompatibles Re-Export des Prisma-Clients
apps/boilerplate/src/lib/db/supabase.tsSupabase-Client für Storage- und Auth-Features
apps/boilerplate/src/lib/db/repository-base.tsAbstrakte Repository-Basisklasse mit Test-Modus-Erkennung
apps/boilerplate/src/lib/db/repositories/user-repository.tsBenutzerdatenzugriff mit Performance-Logging
apps/boilerplate/src/lib/db/queries/users.tsBenutzer-CRUD-Operationen
apps/boilerplate/src/lib/db/queries/subscriptions.tsSubscription-Abfragen mit Geschäftslogik
apps/boilerplate/src/lib/db/queries/files.tsDatei-Metadaten-CRUD mit Paginierung und Suche
apps/boilerplate/src/lib/db/queries/email-logs.tsE-Mail-Tracking, Analytics und DSGVO-Bereinigung
apps/boilerplate/prisma/setup-pgvector.sqlSQL-Skript zur Aktivierung der pgvector-Erweiterung und Erstellung von Indexes