Schema & Modelle

Vollständige Prisma-Schema-Referenz — alle 10 Modelle, Relationen, Indexes und pgvector-Konfiguration

Das Datenbankschema liegt in einer einzigen Datei — apps/boilerplate/prisma/schema.prisma — und definiert alle Tabellen, Spalten, Relationen und Indexes. Prisma liest diese Datei, um einen vollständig typisierten TypeScript-Client zu generieren, sodass jede Abfrage in deiner Anwendung zur Kompilierzeit typgeprüft wird.
Diese Seite geht durch jedes Modell im Schema. Für die Architektur-Übersicht, siehe Datenbank-Übersicht. Für Informationen zur Schema-Änderung, siehe Migrationen & Seeding.

Schema-Konfiguration

Der Anfang der Schema-Datei konfiguriert den Prisma-Client-Generator und die PostgreSQL-Datenquelle:
prisma/schema.prisma — Configuration
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]
}
Wichtige Punkte:
  • binaryTargets — Enthält rhel-openssl-3.0.x für die Bereitstellung in Container-Umgebungen (Docker, AWS Lambda).
  • previewFeatures — Aktiviert postgresqlExtensions, damit Prisma die vector-Erweiterung in der Datenquelle erkennt.
  • extensions = [vector] — Aktiviert die pgvector-Erweiterung für semantische Suche. Diese muss auch in Supabase via SQL aktiviert werden (siehe Migrationen & Seeding).
  • Duale URLsurl für gepoolte Abfragen, directUrl für Migrationen. Siehe die Übersicht für Details.

User-Modell

Das User-Modell ist die zentrale Entität. Es verbindet sich mit Clerk über clerkId und unterstützt Kits duales Preismodell — sowohl kreditbasierte als auch klassische SaaS-Subscription-Felder koexistieren auf demselben Modell. Welches Preismodell aktiv ist, hängt von deiner Konfiguration ab, nicht vom Schema.
prisma/schema.prisma — User Model
model User {
  id                 String              @id @default(uuid())
  clerkId            String              @unique
  email              String?             // Cached from Clerk for payment processing
  name               String?             // Cached from Clerk for payment processing
  hasUsedTrial       Boolean             @default(false) // Track if user has ever used a trial
  downgradeCount     Int                 @default(0) // Track downgrades during trial (max 2)
  lastPlanChangeAt   DateTime?           // Last plan change timestamp
  tier               String              @default("free") // Subscription tier: free, basic, pro, enterprise
  // ============================================
  // CREDIT-BASED MODEL FIELDS (nullable for lazy initialization)
  // ============================================
  creditBalance      Decimal?            @db.Decimal(10, 2) // Current available credits (null = not initialized, supports fractional: 0.2, 0.5, 1.5)
  creditsPerMonth    Decimal?            @db.Decimal(10, 2) // Monthly credit allocation (tier-based, supports fractional)
  creditsResetAt     DateTime?           // Last monthly reset timestamp
  bonusCredits       Decimal?            @db.Decimal(10, 2) // Bonus credits from purchases (top-ups)
  bonusCreditsAutoUse Boolean            @default(false) // Per-user bonus credit toggle (opt-in, like Claude Desktop "Extra Usage")
  // ============================================
  // CLASSIC SAAS MODEL FIELDS
  // ============================================
  isTrial            Boolean             @default(false) // User is currently in trial period
  trialStartDate     DateTime?           // Trial start timestamp
  trialEndDate       DateTime?           // Trial expiration timestamp
  isLocked           Boolean             @default(false) // Account locked after trial expiry
  // ============================================
  // SHARED FIELDS
  // ============================================
  createdAt          DateTime            @default(now())
  updatedAt          DateTime            @updatedAt
  // Relations
  emailLogs          EmailLog[]
  files              File[]
  subscription       Subscription?
  aiUsage            AIUsage[]           // AI usage tracking
  aiConversations    AIConversation[]    // AI conversation history
  creditTransactions CreditTransaction[] // Credit transaction log
  bonusCreditPurchases BonusCreditPurchase[] // Bonus credit purchase history (credit_based only)

  @@index([clerkId])
  @@index([tier])
}

Feldgruppen

Identität (synchronisiert von Clerk):
  • clerkId — Eindeutiger Bezeichner von Clerk. So sucht deine App nach der Authentifizierung nach Benutzern.
  • email, name — Zwischengespeichert aus Clerk-Webhooks für die Zahlungsabwicklung und Anzeige. Wird nicht für die Authentifizierung verwendet.
Kreditbasiertes Preismodell:
  • creditBalance — Aktuell verfügbare Credits. Nullable für Lazy Initialization (Credits werden bei der ersten Nutzung zugewiesen, nicht bei der Registrierung).
  • creditsPerMonth — Monatliche Credit-Zuteilung basierend auf dem Tier des Benutzers.
  • creditsResetAt — Wann das nächste monatliche Reset stattfindet.
  • bonusCredits — Zusätzliche Credits, die als Top-ups gekauft wurden.
  • bonusCreditsAutoUse — Benutzerspezifischer Umschalter für den automatischen Verbrauch von Bonus-Credits (Standard: false). Nur relevant, wenn NEXT_PUBLIC_BONUS_CREDITS_USER_TOGGLE_ENABLED=true.
Klassisches SaaS-Preismodell:
  • isTrial, trialStartDate, trialEndDate — Verfolgung des Testzeitraums.
  • isLocked — Konto gesperrt nach Ablauf des Testzeitraums ohne Umstieg auf ein kostenpflichtiges Modell.
Gemeinsame Felder:
  • tier — Aktuelles Subscription-Tier: free, basic, pro oder enterprise.
  • hasUsedTrial, downgradeCount, lastPlanChangeAt — Geschäftsregel-Felder, die vom Zahlungssystem verwendet werden, um Trial-Missbrauch zu verhindern und Plan-Änderungen zu verfolgen.

Indexes

prisma
@@index([clerkId])  // Schnelle Suche nach Clerk-Authentifizierung
@@index([tier])     // Benutzer nach Subscription-Tier filtern (Admin-Abfragen)

Subscription-Modell

Das Subscription-Modell verfolgt den Lemon Squeezy Zahlungsstatus. Jeder Benutzer kann höchstens eine aktive Subscription haben (erzwungen durch @unique auf userId).
prisma/schema.prisma — Subscription Model
model Subscription {
  id                   String    @id @default(uuid())
  userId               String    @unique
  customerId           String    @unique
  subscriptionId       String    @unique
  productId            String
  variantId            String
  status               String
  currentPeriodEnd     DateTime?
  trialEndsAt          DateTime? // Renamed for consistency with Lemon Squeezy
  canceledAt           DateTime?
  previousVariantId    String?   // Track previous plan for downgrades
  planHistory          String[]  @default([]) // Track all plan changes
  metadata             Json?
  createdAt            DateTime  @default(now())
  updatedAt            DateTime  @updatedAt
  user                 User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([status])
  @@index([customerId])
  @@index([subscriptionId])
}

Wichtige Felder

  • customerId, subscriptionId — Lemon Squeezy-Bezeichner. Beide sind @unique für direkte Suchen aus Webhook-Events.
  • productId, variantId — Verweisen auf spezifische Pläne in deinem Lemon Squeezy-Dashboard. Die Variante bestimmt das Preistier.
  • status — Spiegelt Lemon Squeezy-Zustände wider: active, on_trial, cancelled, expired, past_due, paused.
  • previousVariantId — Verfolgt den letzten Plan vor einer Änderung und ermöglicht Downgrade-Erkennung.
  • planHistory — Nur-anhängen-Array aller Plan-Änderungen. Nützlich für Analytics und Kundensupport.
  • currentPeriodEnd — Wann der aktuelle Abrechnungszeitraum endet. Gekündigte Subscriptions gewähren bis zu diesem Datum weiterhin Zugang.
  • trialEndsAt — Endzeitpunkt des Testzeitraums. Wird gesetzt, wenn eine Test-Subscription über den subscription_created-Webhook erstellt wird.
  • canceledAt — Zeitstempel, wann die Subscription gekündigt wurde. Subscriptions bleiben bis currentPeriodEnd aktiv.
  • metadata — JSON-Metadaten vom Zahlungsanbieter. Speichert zusätzlichen Kontext aus Lemon Squeezy-Webhook-Events.

File-Modell

Das File-Modell speichert Metadaten für Dateien, die in Vercel Blob hochgeladen wurden. Der eigentliche Dateiinhalt liegt im Blob-Storage — nur die Referenz-URL und Metadaten befinden sich in der Datenbank.
prisma
model File {
  id           String   @id @default(uuid())
  userId       String
  url          String          // Vercel Blob URL
  pathname     String          // Storage path
  originalName String          // Display name
  contentType  String?         // MIME type
  size         Int?            // File size in bytes
  metadata     Json?           // Custom metadata
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@index([createdAt])
}

EmailLog-Modell

Das EmailLog-Modell verfolgt jede über Resend gesendete E-Mail. Es unterstützt Analytics (Zustellraten, Bounce-Tracking) und DSGVO-Konformität (automatisierte Log-Bereinigung).
prisma
model EmailLog {
  id        String    @id @default(uuid())
  userId    String?           // Nullable for system emails
  to        String
  from      String?
  subject   String
  type      String?           // "welcome", "password_reset", "invoice", etc.
  status    String            // "sent", "delivered", "bounced", "failed"
  provider  String    @default("resend")
  messageId String?           // Resend message ID for tracking
  metadata  Json?
  sentAt    DateTime?
  error     String?           // Error message if delivery failed
  createdAt DateTime  @default(now())
  user      User?     @relation(fields: [userId], references: [id])

  @@index([userId])
  @@index([to])
  @@index([status])
  @@index([type])
  @@index([createdAt])
}
Das Query-Modul (queries/email-logs.ts) enthält eine Anti-Spam-Prüfung — wasEmailRecentlySent() —, die doppelte Sendungen innerhalb eines konfigurierbaren Zeitfensters verhindert. Es bietet auch deleteOldEmailLogs() für DSGVO-konforme Log-Aufbewahrung.

KI-Modelle

Drei Modelle verfolgen KI-Nutzung, Konversationen und einzelne Nachrichten. Sie unterstützen Kits Multi-Provider-KI-System (OpenAI, Anthropic, Google, xAI).

AIUsage

Verfolgt den KI-Verbrauch pro Anfrage für Kostenmanagement und Analytics:
prisma/schema.prisma — AIUsage
model AIUsage {
  id        String   @id @default(uuid())
  userId    String?
  user      User?    @relation(fields: [userId], references: [id], onDelete: Cascade)
  sessionId String?  // For anonymous users or session-based tracking
  provider  String   // openai, anthropic, google, xai
  model     String   // gpt-5.2, claude-opus-4-6, gemini-2.5-pro, grok-4-1-fast, etc.
  tokens    Int      // Total tokens consumed (prompt + completion)
  cost      Float?   // Estimated cost in USD
  purpose   String   // faq, chat, completion, stream, embedding
  metadata  Json?    // Additional context (message count, temperature, etc.)
  createdAt DateTime @default(now())

  @@index([userId, createdAt])
  @@index([sessionId, createdAt])
  @@index([provider])
  @@index([purpose])
  @@index([createdAt])
}
Das purpose-Feld kategorisiert die Nutzung (faq, chat, completion, stream, embedding) und ermöglicht Feature-spezifische Kostenaufschlüsselungen in deinem Admin-Dashboard.

AIConversation und AIMessage

Konversations-Threads und ihre Nachrichten:
prisma/schema.prisma — AIConversation & AIMessage
model AIConversation {
  id        String      @id @default(uuid())
  userId    String?
  user      User?       @relation(fields: [userId], references: [id], onDelete: Cascade)
  sessionId String      // For session-tracking (even for auth users)
  userTier  String?     // free, pro, enterprise - for rate limiting context
  title     String?     // Optional conversation title (first question)
  createdAt DateTime    @default(now())
  updatedAt DateTime    @updatedAt
  messages  AIMessage[]

  @@index([userId, createdAt])
  @@index([sessionId])
  @@index([createdAt])
}

model AIMessage {
  id             String         @id @default(uuid())
  conversationId String
  conversation   AIConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
  role           String         // user, assistant, system
  content        String         @db.Text
  tokens         Int?           // Token count for cost tracking
  model          String?        // gpt-5.2, claude-opus-4-6, etc.
  provider       String?        // openai, anthropic, google, xai
  metadata       Json?          // Additional context (temperature, max_tokens, etc.)
  createdAt      DateTime       @default(now())

  @@index([conversationId, createdAt])
}
Das sessionId-Feld ermöglicht Konversations-Tracking für sowohl authentifizierte als auch anonyme Benutzer. Das userTier-Feld in Konversationen erlaubt der Rate-Limiting-Logik, den Plan des Benutzers zu prüfen, ohne die User-Tabelle zu joinen.

FAQChunk-Modell (RAG)

Das FAQChunk-Modell speichert Wissensdatenbank-Einträge mit Vektor-Embeddings für semantische Suche. Dies ist die Grundlage von Kits RAG-System (Retrieval-Augmented Generation).
prisma/schema.prisma — FAQChunk
model FAQChunk {
  id         String   @id @default(uuid())
  // Content
  content    String   @db.Text // Full Q&A text
  question   String // Question headline
  answer     String   @db.Text // Answer preview (first 500 chars)
  category   String // "Getting Started", "Database", etc.
  // Vector embedding (OpenAI text-embedding-3-small = 1536 dimensions)
  embedding  Unsupported("vector(1536)")?
  // Metadata
  metadata   Json? // { tags: [], relatedTopics: [], keywords: [] }
  tokenCount Int? // For cost tracking
  // Timestamps
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt

  @@index([category])
  @@index([createdAt])
  @@map("faq_chunks")
}

Das Vektor-Feld

Das embedding-Feld verwendet Unsupported("vector(1536)") — einen Prisma-Escape-Hatch für PostgreSQL-Typen, die Prisma nicht nativ unterstützt. Die Dimension 1536 entspricht OpenAIs text-embedding-3-small-Modell.
Die @@map("faq_chunks")-Direktive ordnet den PascalCase-Prisma-Modellnamen einem snake_case-Tabellennamen in PostgreSQL zu, was der vom pgvector-Index erwarteten Konvention entspricht.

Credit-System-Modelle

Zwei Modelle unterstützen Kits kreditbasiertes Preismodell — ein unveränderliches Transaktions-Ledger und ein Kauf-Tracker.

CreditTransaction

Jede Credit-Änderung wird als unveränderliche Transaktion aufgezeichnet. Dies bietet eine vollständige Prüfspur und ermöglicht die Rekonstruktion des Guthabens aus dem Verlauf:
prisma/schema.prisma — CreditTransaction
model CreditTransaction {
  id           String   @id @default(uuid())
  userId       String
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  amount       Decimal  @db.Decimal(10, 2) // Positive = credit added, Negative = credit deducted (supports fractional: 0.2, 0.5, 1.5)
  balanceAfter Decimal  @db.Decimal(10, 2) // Credit balance after this transaction (supports fractional)
  type         String // "usage", "refund", "monthly_reset", "purchase", "adjustment"
  operation    String? // "faq", "chat", "image_gen", "code_analysis", etc.
  metadata     Json? // Additional context: { model: "gpt-5.2", tokens: 1500, cost: 0.02 }
  createdAt    DateTime @default(now())

  @@index([userId, createdAt])
  @@index([type])
  @@index([createdAt])
}
Transaktionstypen:
TypBeschreibungBetrag
usageCredits, die durch eine KI-Operation verbraucht wurdenNegativ
refundCredits, die aufgrund eines Fehlers oder einer Stornierung zurückgegeben wurdenPositiv
monthly_resetMonatliche Credit-Zuteilung basierend auf dem TierPositiv
purchaseBonus-Credits, die über Lemon Squeezy gekauft wurdenPositiv
adjustmentManuelle Admin-AnpassungBeides
Das operation-Feld liefert Details innerhalb eines Typs — eine usage-Transaktion könnte beispielsweise die Operation chat oder embedding haben.

BonusCreditPurchase

Verfolgt einmalige Credit-Top-up-Käufe über Lemon Squeezy:
prisma/schema.prisma — BonusCreditPurchase
model BonusCreditPurchase {
  id             String   @id @default(uuid())
  userId         String
  user           User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  // Purchase Details
  credits        Int // Number of credits purchased
  price          Decimal  @db.Decimal(10, 2) // Price paid in currency
  expiresAt      DateTime? // Expiration date (null = never expires)
  // Lemon Squeezy Integration
  lemonSqueezyId String   @unique // Order ID from Lemon Squeezy
  variantId      String // Variant ID from Lemon Squeezy
  status         String // "completed", "refunded", "expired"
  // Timestamps
  createdAt      DateTime @default(now())

  @@index([userId])
  @@index([expiresAt])
  @@index([status])
  @@index([createdAt])
}

Relationen-Übersicht

Alle Modelle sind über das User-Modell als zentrale Entität verbunden:
                              User
                               |
          +--------+--------+--+--+--------+--------+--------+
          |        |        |     |        |        |        |
    Subscription  File  EmailLog  |   AIConversation |  BonusCredit
                                  |        |         |   Purchase
                              AIUsage  AIMessage  CreditTransaction
Cascade-Verhalten: Alle Relationen zu User verwenden onDelete: Cascade — das Löschen eines Benutzers entfernt automatisch alle zugehörigen Datensätze. Die einzige Ausnahme ist EmailLog, das eine nullable userId hat (System-E-Mails sind nicht an einen Benutzer gebunden).
Eins-zu-eins: UserSubscription (jeder Benutzer hat höchstens eine Subscription).
Eins-zu-viele: UserFile, EmailLog, AIUsage, AIConversation, CreditTransaction, BonusCreditPurchase.
Verschachtelt eins-zu-viele: AIConversationAIMessage (Nachrichten gehören zu einer Konversation, die zu einem Benutzer gehört).

Indexierungs-Strategie

Kits Indexes sind für die häufigsten Abfragemuster ausgelegt:
MusterIndexWarum
Benutzersuche nach AuthUser.clerkId (unique)Jede authentifizierte Anfrage sucht den Benutzer nach Clerk-ID
Tier-basierte AbfragenUser.tierAdmin-Dashboard filtert Benutzer nach Plan
Subscription-WebhooksSubscription.customerId, subscriptionId (unique)Lemon Squeezy-Webhooks identifizieren Subscriptions über diese Felder
Zeitreihen-AbfragenZusammengesetzt (userId, createdAt) bei AIUsage, CreditTransactionNutzungs-Analytics und Transaktionsverlauf für einen bestimmten Benutzer
E-Mail-AnalyticsEmailLog.status, type, toBerechnung von Zustellraten und empfängerbasierter Verlauf
Subscription-Status-AbfragenSubscription.statusWebhook-Handler und Admin-Abfragen filtern nach Subscription-Status
KI-Session-AnalyticsZusammengesetzt (sessionId, createdAt) bei AIUsageSession-bezogenes Nutzungs-Tracking und Kontingent-Prüfungen
VektorsucheIVFFlat auf faq_chunks.embeddingSchnelle approximative Nearest-Neighbor-Suche (siehe pgvector-Setup)

Eigene Modelle hinzufügen {#adding-your-own-models}

Um ein neues Modell zum Schema hinzuzufügen:
1

Definiere das Modell in schema.prisma

Füge deine Modelldefinition zu apps/boilerplate/prisma/schema.prisma hinzu. Halte dich an die bestehenden Konventionen:
prisma
model BlogPost {
  id        String   @id @default(uuid())
  userId    String
  title     String
  content   String   @db.Text
  slug      String   @unique
  published Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@index([slug])
  @@index([createdAt])
}
Dann füge die umgekehrte Relation zum User-Modell hinzu:
prisma
model User {
  // ... existing fields
  blogPosts  BlogPost[]
}
2

Schema pushen oder migrieren

In der Entwicklung das Schema direkt pushen:
bash
cd apps/boilerplate && npx prisma db push
Für die Produktion eine Migration erstellen:
bash
cd apps/boilerplate && npx prisma migrate dev --name add-blog-post
Siehe Migrationen & Seeding für den vollständigen Workflow.
3

Prisma-Client neu generieren

Nach jeder Schema-Änderung die TypeScript-Typen neu generieren:
bash
cd apps/boilerplate && npx prisma generate
Dies aktualisiert das @prisma/client-Paket, sodass dein neues Modell mit vollständiger Typsicherheit verfügbar ist.
4

Query-Modul erstellen

Eine Query-Datei unter apps/boilerplate/src/lib/db/queries/blog-posts.ts anlegen, die dem bestehenden Muster folgt:
typescript
import { prisma } from '@/lib/db/prisma'
import type { BlogPost } from '@prisma/client'

export async function getBlogPostsByUserId(userId: string): Promise<BlogPost[]> {
  return prisma.blogPost.findMany({
    where: { userId },
    orderBy: { createdAt: 'desc' }
  })
}

export async function getBlogPostBySlug(slug: string): Promise<BlogPost | null> {
  return prisma.blogPost.findUnique({
    where: { slug }
  })
}
Aus apps/boilerplate/src/lib/db/queries/index.ts exportieren, um Imports sauber zu halten.