generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" } // ================ ENUMS ================= enum USER_STATUS { PENDING_EMAIL_VERIFICATION PENDING_PHONE_VERIFICATION PENDING_PROFILE_COMPLETION ENABLED DISABLED SUSPENDED INVITED } enum SESSION_STATUS { CURRENT EXPIRED REVOKED } enum OTP_TYPE { EMAIL SMS } enum OTP_PURPOSE { EMAIL_VERIFICATION PHONE_VERIFICATION PASSWORD_RESET LOGIN_2FA } enum UPLOAD_STATUS { UPLOADING PROCESSING COMPLETED FAILED } enum NOTIFICATION_TYPE { NEW_MOVIE NEW_SERIES NEW_EPISODE COMMENT_REPLY COMMENT_LIKE CONTENT_LIKE ACCOUNT_UPDATE SUBSCRIPTION_REMINDER RECOMMENDATION } enum CONTENT_TYPE { MOVIE SERIES EPISODE } enum TWO_FACTOR_METHOD_TYPE { EMAIL AUTHENTICATOR // TOTP PASSKEY // WebAuthn / FIDO2 PUSH_APPROVAL // Mobile yes/no } enum PUSH_APPROVAL_STATUS { PENDING APPROVED DENIED EXPIRED } enum PLATFORM_TYPE { ANDROID IOS WEB } enum URL_SHORTENER_PROVIDER { SHLINK TRIM_KUTT } // ============ USER MANAGEMENT ============ model User { id String @id @default(uuid()) uniqueId String @unique email String? @unique phone String? @unique code String? firstName String? lastName String? firstNameLastName String? lastNameFirstName String? profileImage String? bio String? @db.Text dateOfBirth DateTime? @db.Timestamp(3) country String? city String? status USER_STATUS @default(PENDING_EMAIL_VERIFICATION) emailVerified Boolean @default(false) phoneVerified Boolean @default(false) lastLogin DateTime? @db.Timestamp(3) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations sessions UserSession[] credentials UserCredential[] otps UserOtp[] interests UserInterest[] watchHistories WatchHistory[] notifications Notification[] playlists Playlist[] likes Like[] ratings Rating[] comments Comment[] commentLikes CommentLike[] @@map("users") } model UserSession { id String @id @default(uuid()) userId String accessToken String @db.Text refreshToken String @db.Text ipAddress String? userAgent String? @db.Text geoIpCountry String? city String? state String? latitude Float? longitude Float? status SESSION_STATUS @default(CURRENT) loginAt DateTime? @db.Timestamp(3) logoutAt DateTime? @db.Timestamp(3) expiredAt DateTime? @db.Timestamp(3) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_sessions") } model UserCredential { id String @id @default(uuid()) userId String @unique password String createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_credentials") } model UserOtp { id String @id @default(uuid()) userId String otp String type OTP_TYPE purpose OTP_PURPOSE expiresAt DateTime @db.Timestamp(3) verified Boolean @default(false) verifiedAt DateTime? @db.Timestamp(3) attempts Int @default(0) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, type, verified]) @@map("user_otps") } model UserInterest { id String @id @default(uuid()) userId String genreId String createdAt DateTime @default(now()) @db.Timestamp(3) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) genre Genre @relation(fields: [genreId], references: [id], onDelete: Cascade) @@unique([userId, genreId]) @@index([userId]) @@index([genreId]) @@map("user_interests") } // ============ SUPER ADMIN ============ model SuperAdmin { id String @id @default(uuid()) email String @unique name String profileImage String? isTwoFactorEnabled Boolean @default(false) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations sessions SuperAdminSession[] credentials SuperAdminCredential[] twoFactorMethods SuperAdminTwoFactorMethod[] recoveryCodes SuperAdminRecoveryCode[] trustedDevices SuperAdminTrustedDevice[] webAuthnCredentials SuperAdminWebAuthnCredential[] pushDevices SuperAdminPushDevice[] pushApprovalChallenges SuperAdminPushApprovalChallenge[] @@map("super_admins") } model SuperAdminSession { id String @id @default(uuid()) superAdminId String accessToken String @db.Text refreshToken String @db.Text ipAddress String? userAgent String? @db.Text geoIpCountry String? city String? state String? latitude Float? longitude Float? isFromAdmin Boolean @default(false) status SESSION_STATUS @default(CURRENT) loginAt DateTime? @db.Timestamp(3) expiredAt DateTime? @db.Timestamp(3) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id], onDelete: Cascade) @@map("super_admin_sessions") } model SuperAdminCredential { id String @id @default(uuid()) superAdminId String password String createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id], onDelete: Cascade) @@map("super_admin_credentials") } // ============ SUPER ADMIN 2FA ============ model SuperAdminTwoFactorMethod { id String @id @default(uuid()) superAdminId String type TWO_FACTOR_METHOD_TYPE isEnabled Boolean @default(false) isPrimary Boolean @default(false) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id], onDelete: Cascade) emailTwoFactor SuperAdminEmailTwoFactor? totpTwoFactor SuperAdminTotpTwoFactor? passkeyTwoFactor SuperAdminPasskeyTwoFactor? pushTwoFactor SuperAdminPushTwoFactor? @@unique([superAdminId, type]) @@index([superAdminId]) @@map("super_admin_two_factor_methods") } model SuperAdminEmailTwoFactor { id String @id @default(uuid()) methodId String @unique lastCodeHash String? lastCodeExpiresAt DateTime? @db.Timestamp(3) lastSentAt DateTime? @db.Timestamp(3) failedAttempts Int @default(0) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations method SuperAdminTwoFactorMethod @relation(fields: [methodId], references: [id], onDelete: Cascade) @@map("super_admin_email_two_factor") } model SuperAdminTotpTwoFactor { id String @id @default(uuid()) methodId String @unique secretEncrypted String confirmedAt DateTime? @db.Timestamp(3) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations method SuperAdminTwoFactorMethod @relation(fields: [methodId], references: [id], onDelete: Cascade) @@map("super_admin_totp_two_factor") } model SuperAdminPasskeyTwoFactor { id String @id @default(uuid()) methodId String @unique createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations method SuperAdminTwoFactorMethod @relation(fields: [methodId], references: [id], onDelete: Cascade) @@map("super_admin_passkey_two_factor") } model SuperAdminPushTwoFactor { id String @id @default(uuid()) methodId String @unique createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations method SuperAdminTwoFactorMethod @relation(fields: [methodId], references: [id], onDelete: Cascade) @@map("super_admin_push_two_factor") } model SuperAdminRecoveryCode { id String @id @default(uuid()) superAdminId String codeHash String usedAt DateTime? @db.Timestamp(3) createdAt DateTime @default(now()) @db.Timestamp(3) isDeleted Boolean @default(false) // Relations superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id], onDelete: Cascade) @@index([superAdminId]) @@map("super_admin_recovery_codes") } model SuperAdminWebAuthnCredential { id String @id @default(uuid()) superAdminId String credentialId String @unique publicKey String @db.Text counter Int @default(0) deviceName String? createdAt DateTime @default(now()) @db.Timestamp(3) lastUsedAt DateTime? @db.Timestamp(3) isDeleted Boolean @default(false) // Relations superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id], onDelete: Cascade) @@index([superAdminId]) @@map("super_admin_webauthn_credentials") } model SuperAdminTrustedDevice { id String @id @default(uuid()) superAdminId String deviceId String @unique deviceName String? ipAddress String? userAgent String? @db.Text lastUsedAt DateTime @default(now()) @db.Timestamp(3) expiresAt DateTime @db.Timestamp(3) createdAt DateTime @default(now()) @db.Timestamp(3) isDeleted Boolean @default(false) // Relations superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id], onDelete: Cascade) @@index([superAdminId]) @@index([deviceId]) @@map("super_admin_trusted_devices") } model SuperAdminPushDevice { id String @id @default(uuid()) superAdminId String pushToken String platform PLATFORM_TYPE deviceName String? lastSeenAt DateTime @default(now()) @db.Timestamp(3) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id], onDelete: Cascade) @@index([superAdminId]) @@map("super_admin_push_devices") } model SuperAdminPushApprovalChallenge { id String @id @default(uuid()) superAdminId String status PUSH_APPROVAL_STATUS @default(PENDING) ipAddress String? userAgent String? @db.Text location String? deviceInfo String? expiresAt DateTime @db.Timestamp(3) approvedAt DateTime? @db.Timestamp(3) deniedAt DateTime? @db.Timestamp(3) createdAt DateTime @default(now()) @db.Timestamp(3) isDeleted Boolean @default(false) // Relations superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id], onDelete: Cascade) @@index([superAdminId]) @@index([status]) @@map("super_admin_push_approval_challenges") } // ============ MOVIES ============ model Movie { id String @id @default(uuid()) title String slug String @unique description String? @db.Text thumbnailUrl String bannerUrl String? trailerUrl String? rating String? releaseYear Int? language String? duration Int? imdbId String? tmdbId String? isFeatured Boolean @default(false) uploadStatus UPLOAD_STATUS @default(COMPLETED) views Int @default(0) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations genres MovieGenre[] tags MovieTag[] video MovieVideo? comments Comment[] likes Like[] ratings Rating[] playlistItems PlaylistItem[] watchHistory WatchHistory[] @@index([isDeleted]) @@index([isFeatured]) @@index([slug]) @@map("movies") } model MovieVideo { id String @id @default(uuid()) movieId String @unique videoUrl String videoKey String? fileSize BigInt? quality String? uploadStatus UPLOAD_STATUS @default(COMPLETED) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations movie Movie @relation(fields: [movieId], references: [id], onDelete: Cascade) @@index([isDeleted]) @@map("movie_videos") } model MovieGenre { id String @id @default(uuid()) movieId String genreId String createdAt DateTime @default(now()) @db.Timestamp(3) // Relations movie Movie @relation(fields: [movieId], references: [id], onDelete: Cascade) genre Genre @relation(fields: [genreId], references: [id], onDelete: Cascade) @@unique([movieId, genreId]) @@index([movieId]) @@index([genreId]) @@map("movie_genres") } // ============ SERIES ============ model Series { id String @id @default(uuid()) title String slug String @unique description String? @db.Text thumbnailUrl String bannerUrl String? trailerUrl String? rating String? releaseYear Int? language String? imdbId String? tmdbId String? isFeatured Boolean @default(false) uploadStatus UPLOAD_STATUS @default(COMPLETED) views Int @default(0) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations genres SeriesGenre[] tags SeriesTag[] seasons Season[] comments Comment[] likes Like[] ratings Rating[] playlistItems PlaylistItem[] watchHistory WatchHistory[] @@index([isDeleted]) @@index([isFeatured]) @@index([slug]) @@map("series") } model Season { id String @id @default(uuid()) seriesId String seasonNumber Int title String description String? @db.Text thumbnailUrl String? releaseYear Int? createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations series Series @relation(fields: [seriesId], references: [id], onDelete: Cascade) episodes Episode[] @@unique([seriesId, seasonNumber]) @@index([seriesId]) @@map("seasons") } model Episode { id String @id @default(uuid()) seasonId String episodeNumber Int title String description String? @db.Text thumbnailUrl String? duration Int createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations season Season @relation(fields: [seasonId], references: [id], onDelete: Cascade) video EpisodeVideo? comments Comment[] likes Like[] ratings Rating[] playlistItems PlaylistItem[] watchHistory WatchHistory[] @@unique([seasonId, episodeNumber]) @@index([seasonId]) @@map("episodes") } model EpisodeVideo { id String @id @default(uuid()) episodeId String @unique videoUrl String videoKey String? fileSize BigInt? quality String? uploadStatus UPLOAD_STATUS @default(COMPLETED) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade) @@index([isDeleted]) @@map("episode_videos") } model SeriesGenre { id String @id @default(uuid()) seriesId String genreId String createdAt DateTime @default(now()) @db.Timestamp(3) // Relations series Series @relation(fields: [seriesId], references: [id], onDelete: Cascade) genre Genre @relation(fields: [genreId], references: [id], onDelete: Cascade) @@unique([seriesId, genreId]) @@index([seriesId]) @@index([genreId]) @@map("series_genres") } // ============ GENRES ============ model Genre { id String @id @default(uuid()) name String @unique slug String @unique description String? @db.Text icon String? sortOrder Int @default(0) createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations movieGenres MovieGenre[] seriesGenres SeriesGenre[] userInterests UserInterest[] @@index([slug]) @@map("genres") } // ============ UNIFIED COMMENT ============ model Comment { id String @id @default(uuid()) content String @db.Text userId String contentType CONTENT_TYPE movieId String? seriesId String? episodeId String? parentId String? createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) movie Movie? @relation(fields: [movieId], references: [id], onDelete: Cascade) series Series? @relation(fields: [seriesId], references: [id], onDelete: Cascade) episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade) parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade) replies Comment[] @relation("CommentReplies") likes CommentLike[] @@index([movieId, isDeleted]) @@index([seriesId, isDeleted]) @@index([episodeId, isDeleted]) @@index([userId, isDeleted]) @@index([contentType, isDeleted]) @@map("comments") } // ============ UNIFIED LIKE ============ model Like { id String @id @default(uuid()) userId String contentType CONTENT_TYPE movieId String? seriesId String? episodeId String? isLike Boolean createdAt DateTime @default(now()) @db.Timestamp(3) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) movie Movie? @relation(fields: [movieId], references: [id], onDelete: Cascade) series Series? @relation(fields: [seriesId], references: [id], onDelete: Cascade) episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade) // Only one of movieId/seriesId/episodeId should be non-null per row (enforce in code) @@unique([userId, movieId]) @@unique([userId, seriesId]) @@unique([userId, episodeId]) @@index([movieId, contentType]) @@index([seriesId, contentType]) @@index([episodeId, contentType]) @@map("likes") } // ============ COMMENT LIKE ============ model CommentLike { id String @id @default(uuid()) userId String commentId String isLike Boolean createdAt DateTime @default(now()) @db.Timestamp(3) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) @@unique([userId, commentId]) @@index([commentId]) @@map("comment_likes") } // ============ PLAYLISTS ============ model Playlist { id String @id @default(uuid()) name String description String? @db.Text userId String createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) items PlaylistItem[] @@index([userId, isDeleted]) @@map("playlists") } model PlaylistItem { id String @id @default(uuid()) playlistId String contentType CONTENT_TYPE movieId String? seriesId String? episodeId String? position Int createdAt DateTime @default(now()) @db.Timestamp(3) // Relations playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade) movie Movie? @relation(fields: [movieId], references: [id], onDelete: Cascade) series Series? @relation(fields: [seriesId], references: [id], onDelete: Cascade) episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade) // Only one of movieId/seriesId/episodeId should be non-null per row (enforce in code) @@unique([playlistId, movieId]) @@unique([playlistId, seriesId]) @@unique([playlistId, episodeId]) @@index([playlistId]) @@index([movieId, contentType]) @@index([seriesId, contentType]) @@index([episodeId, contentType]) @@map("playlist_items") } // ============ WATCH HISTORY ============ model WatchHistory { id String @id @default(uuid()) userId String contentType CONTENT_TYPE movieId String? seriesId String? episodeId String? watchedAt DateTime @default(now()) @db.Timestamp(3) watchTime Int duration Int completed Boolean @default(false) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) movie Movie? @relation(fields: [movieId], references: [id], onDelete: Cascade) series Series? @relation(fields: [seriesId], references: [id], onDelete: Cascade) episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade) @@index([userId, contentType]) @@index([movieId]) @@index([seriesId]) @@index([episodeId]) @@map("watch_history") } // ============ NOTIFICATIONS ============ model Notification { id String @id @default(uuid()) userId String type NOTIFICATION_TYPE title String message String @db.Text read Boolean @default(false) data Json? createdAt DateTime @default(now()) @db.Timestamp(3) isDeleted Boolean @default(false) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, isDeleted]) @@index([userId, read, isDeleted]) @@map("notifications") } // ============ RATINGS (Star Ratings 1-5) ============ model Rating { id String @id @default(uuid()) userId String contentType CONTENT_TYPE movieId String? seriesId String? episodeId String? value Int createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) movie Movie? @relation(fields: [movieId], references: [id], onDelete: Cascade) series Series? @relation(fields: [seriesId], references: [id], onDelete: Cascade) episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade) // Only one of movieId/seriesId/episodeId should be non-null per row (enforce in code) @@unique([userId, movieId]) @@unique([userId, seriesId]) @@unique([userId, episodeId]) @@index([movieId, contentType]) @@index([seriesId, contentType]) @@index([episodeId, contentType]) @@map("ratings") } // ============ UPLOADS ============ model Upload { id String @id @default(uuid()) fileName String fileType String // 'image', 'video', 'document' originalSize Int? // For videos (HLS) hlsUrl String? // URL to playlist.m3u8 hlsFolderId String? // folder containing HLS files hlsSegments Json? // Array of segment URLs videoDuration Float? // Duration in seconds status String @default("pending") // pending, processing, completed, failed errorMessage String? uploadedAt DateTime @default(now()) @db.Timestamp(3) processedAt DateTime? metadata Json? @@index([status]) @@index([fileType]) @@index([uploadedAt]) @@map("uploads") } // ============ TAGS (Flexible Content Tags) ============ model Tag { id String @id @default(uuid()) name String @unique slug String @unique createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) // Relations movieTags MovieTag[] seriesTags SeriesTag[] @@index([slug]) @@map("tags") } model MovieTag { id String @id @default(uuid()) movieId String tagId String createdAt DateTime @default(now()) @db.Timestamp(3) // Relations movie Movie @relation(fields: [movieId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@unique([movieId, tagId]) @@index([movieId]) @@index([tagId]) @@map("movie_tags") } model SeriesTag { id String @id @default(uuid()) seriesId String tagId String createdAt DateTime @default(now()) @db.Timestamp(3) // Relations series Series @relation(fields: [seriesId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@unique([seriesId, tagId]) @@index([seriesId]) @@index([tagId]) @@map("series_tags") } // ============ SHORT URL ============ model ShortUrl { id String @id @default(uuid()) longUrl String @db.Text shortUrl String shortCode String? externalId String? // Provider's internal ID (e.g., Kutt's ID) provider URL_SHORTENER_PROVIDER createdAt DateTime @default(now()) @db.Timestamp(3) updatedAt DateTime @updatedAt @db.Timestamp(3) isDeleted Boolean @default(false) @@index([provider]) @@map("short_urls") }