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]
}
Supabase leitet gepoolte Verbindungen über pgbouncer im Transaction-Modus weiter, der Session-Level-Features wie
SET-Anweisungen und Advisory Locks entfernt. Prisma-Migrationen benötigen diese Features und brauchen daher eine direkte Verbindung, die den Pooler umgeht. In der Entwicklung mit einer lokalen Datenbank können beide URLs auf dieselbe Verbindung zeigen.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:
- Hot-Reload-Schutz — In der Entwicklung wird der Client auf
globalThisgespeichert, sodass Hot Module Replacement bei jeder Dateiänderung keine neuen Datenbankverbindungen erstellt. - Test-/Demo-Modus — Wenn
NEXT_PUBLIC_DEMO_MODE=trueoderNEXT_PUBLIC_CLERK_ENABLED=falsegesetzt ist, wird stattdessen ein Mock-Proxy-Client zurückgegeben. Dadurch kann die gesamte Anwendung ohne Datenbankverbindung für E2E-Tests und Demos betrieben werden. - 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' }
})
})
Welche Ebene solltest du verwenden? Beginne mit Query-Modulen für Standard-CRUD. Verwende Repositories, wenn du Test-Modus-Kompatibilität für Leseoperationen benötigst. Verwende direkten Prisma-Zugriff für Transaktionen, Raw SQL oder ebenenübergreifende Operationen, die nicht in eine einzelne Query-Funktion passen.
Best Practice für Server Components
Die Verwendung von
fetch() zum Aufrufen eigener API-Routen aus Server Components verursacht katastrophale Performance-Einbußen — Seitenladezeiten steigen von ~400ms auf 7+ Sekunden. Der Next.js-Dev-Server hat eine begrenzte Request-Parallelität, und ein Server Component, der eine HTTP-Anfrage an denselben Server stellt, blockiert und wartet auf seine eigene Antwort. Mehrere Prefetch-Aufrufe potenzieren das Problem exponentiell.Gemessene Auswirkung: Die Dashboard-Ladezeit sank von 7.429ms auf 445ms (94% Verbesserung), indem von
fetch('/api/subscription') auf direkte getSubscriptionByUserId()-Aufrufe umgestellt wurde.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.
| Modell | Zweck | Wichtige Relationen |
|---|---|---|
| User | Zentrales Benutzer-Entity mit Clerk-Sync, unterstützt duales Preismodell (Credits + klassisches SaaS) | Hat viele: Subscription, File, EmailLog, AIUsage, AIConversation, CreditTransaction, BonusCreditPurchase |
| Subscription | Lemon Squeezy Subscription-Tracking mit Plan-Verlauf | Gehört zu User (Cascade Delete) |
| CreditTransaction | Unveränderliches Credit-Ledger (Nutzung, Rückerstattungen, Resets, Käufe) | Gehört zu User |
| BonusCreditPurchase | Top-up-Credit-Käufe über Lemon Squeezy | Gehört zu User |
| AIUsage | KI-Token- und Kosten-Tracking pro Anfrage | Gehört zu User |
| AIConversation | Chat-Konversations-Threads mit Session-Tracking | Gehört zu User, hat viele AIMessage |
| AIMessage | Einzelne Chat-Nachrichten (User, Assistant, System) | Gehört zu AIConversation |
| FAQChunk | RAG-Wissensbasis mit pgvector-Embeddings (1536 Dimensionen) | Eigenständig |
| File | Vercel Blob Datei-Upload-Metadaten | Gehört zu User |
| EmailLog | Resend E-Mail-Zustellungs-Tracking mit Analytics | Gehört zu User |
Umgebungsvariablen
| Variable | Erforderlich | Zweck |
|---|---|---|
DATABASE_URL | Ja | Gepoolte PostgreSQL-Verbindung (pgbouncer) für Anwendungsabfragen |
DIRECT_URL | Ja | Direkte PostgreSQL-Verbindung für Prisma-Migrationen |
NEXT_PUBLIC_SUPABASE_URL | Ja | Supabase-Projekt-URL (für Storage und andere Supabase-Features) |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Ja | Supabase Anonymous Key (öffentlich, sicher für Client-Seite) |
SUPABASE_SERVICE_ROLE_KEY | Nein | Supabase Service Role Key (nur Server, umgeht RLS) |
Wichtige Dateien
| Datei | Zweck |
|---|---|
apps/boilerplate/prisma/schema.prisma | Datenbankschema — alle Modelle, Relationen, Indexes und Konfiguration |
apps/boilerplate/src/lib/db/prisma.ts | Prisma-Client-Singleton mit Test-Modus und Graceful Shutdown |
apps/boilerplate/src/lib/db/client.ts | Abwärtskompatibles Re-Export des Prisma-Clients |
apps/boilerplate/src/lib/db/supabase.ts | Supabase-Client für Storage- und Auth-Features |
apps/boilerplate/src/lib/db/repository-base.ts | Abstrakte Repository-Basisklasse mit Test-Modus-Erkennung |
apps/boilerplate/src/lib/db/repositories/user-repository.ts | Benutzerdatenzugriff mit Performance-Logging |
apps/boilerplate/src/lib/db/queries/users.ts | Benutzer-CRUD-Operationen |
apps/boilerplate/src/lib/db/queries/subscriptions.ts | Subscription-Abfragen mit Geschäftslogik |
apps/boilerplate/src/lib/db/queries/files.ts | Datei-Metadaten-CRUD mit Paginierung und Suche |
apps/boilerplate/src/lib/db/queries/email-logs.ts | E-Mail-Tracking, Analytics und DSGVO-Bereinigung |
apps/boilerplate/prisma/setup-pgvector.sql | SQL-Skript zur Aktivierung der pgvector-Erweiterung und Erstellung von Indexes |