generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum Plan { FREE PREMIUM } enum Privacy { PUBLIC PRIVATE UNLISTED } enum JobStatus { PENDING PROCESSING COMPLETED FAILED PARTIAL } enum JobItemStatus { PENDING ENCODING UPLOADING COMPLETED FAILED } enum QuotaExtensionRequestStatus { PENDING APPROVED REJECTED } enum QuotaExtensionKind { VIDEO_QUOTA API_RATE_LIMIT } model User { id String @id @default(cuid()) email String @unique name String? image String? /// FREE = free tier; PREMIUM = Pro (€5/mo, formerly starter_5eur) plan Plan @default(FREE) /// Credits consumed in the current billing cycle (credits_used) videosUsed Int @default(0) /// Monthly allocation for the current cycle (10 free / 50 pro) monthlyCredits Int @default(10) quotaResetAt DateTime cardLast4 String? subscribedAt DateTime? bonusQuota Int @default(0) apiRateLimitBonus Int @default(0) /// Purchased extras that never expire (maps to legacy "videoCredits" column) extraCredits Int @default(0) @map("videoCredits") /// Free-tier one-time +15 top-up already purchased freeTopUpPurchased Boolean @default(false) stripeCustomerId String? stripeSubscriptionId String? apiKeyHash String? @unique apiKeyPrefix String? youtubeConnection YouTubeConnection? jobs Job[] quotaExtensionRequests QuotaExtensionRequest[] creditPurchases CreditPurchase[] createdAt DateTime @default(now()) } enum CreditPurchaseStatus { PENDING COMPLETED FAILED } model CreditPurchase { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) credits Int amountCents Int status CreditPurchaseStatus @default(PENDING) stripeSessionId String? @unique stripePaymentIntentId String? createdAt DateTime @default(now()) completedAt DateTime? } enum JobItemBilling { QUOTA CREDIT } model QuotaExtensionRequest { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) kind QuotaExtensionKind @default(VIDEO_QUOTA) status QuotaExtensionRequestStatus @default(PENDING) message String @default("") requestedAt DateTime @default(now()) processedAt DateTime? adminNote String? } model YouTubeConnection { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) accessToken String refreshToken String expiresAt DateTime channelId String channelTitle String } model Job { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) status JobStatus @default(PENDING) imagePath String items JobItem[] createdAt DateTime @default(now()) completedAt DateTime? } model JobItem { id String @id @default(cuid()) jobId String job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) audioPath String audioFilename String title String description String @default("") tags String @default("") privacy Privacy @default(PUBLIC) categoryId String @default("10") resolution String @default("1280x720") notifySubscribers Boolean @default(true) madeForKids Boolean @default(false) embeddable Boolean @default(true) creativeCommons Boolean @default(false) includeWatermark Boolean @default(true) /// Optional per-item cover (Pro). Falls back to Job.imagePath when null. itemImagePath String? /// none | default | text | logo custom modes require Pro watermarkMode String @default("default") watermarkText String? watermarkLogoPath String? /// curated key | custom | system watermarkFontKey String? /// uploaded .ttf/.otf when fontKey=custom watermarkFontPath String? /// top-left | top-right | bottom-left | bottom-right | center watermarkPosition String @default("bottom-right") watermarkOffsetX Int @default(20) watermarkOffsetY Int @default(20) /// Optional artist line for art-track layouts (Pro, on-video only) artist String? /// Song title burned into art-track layouts (Pro, on-video only) songTitle String? /// COVER_LEFT_TEXT_RIGHT | COVER_TOP_TEXT_BOTTOM | COVER_RIGHT_TEXT_LEFT | CENTERED_COMPACT | null=classic layoutTemplate String? /// 0–100 Gaussian / boxblur intensity (Pro art-track) blurAmount Int @default(55) /// 0–100 visibility of blurred fill vs black blurOpacity Int @default(100) /// Padding around cover + text in art-track layouts textPadding Int @default(48) /// Extra gap between title and artist lines (px) titleArtistGap Int @default(10) /// Fine-tune text block within template (-120..120) textOffsetX Int @default(0) textOffsetY Int @default(0) playlistId String? billingSource JobItemBilling @default(QUOTA) status JobItemStatus @default(PENDING) outputPath String? youtubeVideoId String? error String? } /// Site footer badge / launch-directory embeds (managed via /admin) model FooterBadge { id String @id @default(cuid()) name String /// Raw paste from Product Hunt / LaunchBuff / etc. (preferred source) embedHtml String? @db.Text /// Parsed or manually set image URL (used when embedHtml is empty) imageUrl String? /// Parsed or manually set destination URL linkUrl String? isActive Boolean @default(true) sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([isActive, sortOrder]) }