generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ─── Auth.js Models ────────────────────────────────────────────── model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? password String? // hashed password for credentials auth createdAt DateTime @default(now()) updatedAt DateTime @updatedAt accounts Account[] sessions Session[] drinks Drink[] ratings Rating[] menuScans MenuScan[] apiKeys UserApiKey[] preferences UserPreference? wishlistItems WishlistItem[] sharedLists SharedList[] barItems BarItem[] recipes Recipe[] flavorProfile FlavorProfile? } model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) } model Session { id String @id @default(cuid()) sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model VerificationToken { identifier String token String @unique expires DateTime @@unique([identifier, token]) } // ─── App Models ────────────────────────────────────────────────── model UserApiKey { id String @id @default(cuid()) userId String provider String // "claude" | "openai" encryptedKey String @db.Text iv String // initialization vector for decryption label String? // optional user-friendly label isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([userId, provider]) } model UserPreference { id String @id @default(cuid()) userId String @unique preferredStyles String[] // e.g., ["IPA", "Stout", "Pinot Noir"] avoidedStyles String[] // e.g., ["Sour", "Light Lager"] minAbv Float? maxAbv Float? defaultProvider String? // preferred AI provider createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) } enum DrinkType { BEER WINE COCKTAIL SPIRIT OTHER } model Drink { id String @id @default(cuid()) userId String name String type DrinkType subType String? // e.g., "IPA", "Stout", "Cabernet Sauvignon" brewery String? // brewery or winery region String? abv Float? description String? @db.Text imageUrl String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) ratings Rating[] menuItems MenuItem[] recipes Recipe[] @@index([userId]) @@index([userId, type]) @@index([userId, name]) } model Rating { id String @id @default(cuid()) userId String drinkId String score Int // 1-5 notes String? @db.Text wouldReorder Boolean @default(false) location String? // where they tried it createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) drink Drink @relation(fields: [drinkId], references: [id], onDelete: Cascade) @@index([userId]) @@index([drinkId]) } enum ScanStatus { UPLOADING PROCESSING COMPLETED FAILED } model MenuScan { id String @id @default(cuid()) userId String imageUrl String status ScanStatus @default(UPLOADING) aiProvider String? // which provider was used aiRawResponse Json? // raw AI response for debugging errorMessage String? @db.Text processedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) items MenuItem[] @@index([userId]) } model MenuItem { id String @id @default(cuid()) scanId String name String type DrinkType subType String? brewery String? abv Float? price String? description String? @db.Text matchedDrinkId String? userRating Int? // cached rating from matched drink aiRecommended Boolean @default(false) aiReason String? @db.Text // why AI recommends it createdAt DateTime @default(now()) scan MenuScan @relation(fields: [scanId], references: [id], onDelete: Cascade) matchedDrink Drink? @relation(fields: [matchedDrinkId], references: [id], onDelete: SetNull) @@index([scanId]) } // ─── Wishlist / Try Later ──────────────────────────────────────── model WishlistItem { id String @id @default(cuid()) userId String name String type DrinkType subType String? brewery String? abv Float? description String? @db.Text notes String? @db.Text // user's personal note ("saw at Joe's bar") source String? // "scan", "ai_search", "manual" createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) } // ─── Search Cache ─────────────────────────────────────────────── model SearchCache { id String @id @default(cuid()) userId String queryHash String // normalized (lowercase, trimmed) query String // original text results Json // { drinks: [...] } provider String // "claude" | "openai" createdAt DateTime @default(now()) @@unique([userId, queryHash, provider]) @@index([userId]) } // ─── Shared Lists ──────────────────────────────────────────────── model SharedList { id String @id @default(cuid()) userId String slug String @unique // public URL slug title String description String? @db.Text listType String @default("collection") // "collection", "wishlist", "custom" isPublic Boolean @default(true) drinkIds String[] // ids of drinks to include (empty = all) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([slug]) } // ─── Bar Inventory ────────────────────────────────────────────── enum BarItemCategory { SPIRITS LIQUEURS MIXERS BITTERS GARNISHES TOOLS } enum BarItemQuantity { FULL HALF LOW EMPTY } model BarItem { id String @id @default(cuid()) userId String name String category BarItemCategory quantity BarItemQuantity @default(FULL) notes String? @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([userId, category]) } // ─── Recipes ──────────────────────────────────────────────────── model Recipe { id String @id @default(cuid()) userId String title String ingredients Json // [{ name: string, amount: string, available: boolean }] steps Json // string[] garnish String? glassware String? sourceDrinkId String? notes String? @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) sourceDrink Drink? @relation(fields: [sourceDrinkId], references: [id], onDelete: SetNull) @@index([userId]) } // ─── Flavor Profile ───────────────────────────────────────────── model FlavorProfile { id String @id @default(cuid()) userId String @unique profileText String @db.Text profileData Json? generatedAt DateTime @default(now()) ratingCount Int @default(0) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) }