changemaker.lite/api/prisma/schema.prisma
2026-02-18 10:01:54 -07:00

3462 lines
125 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================================================
// AUTH & USERS
// ============================================================================
enum UserRole {
SUPER_ADMIN
INFLUENCE_ADMIN
MAP_ADMIN
USER
TEMP
}
enum UserStatus {
ACTIVE
INACTIVE
SUSPENDED
EXPIRED
PENDING_VERIFICATION
PENDING_APPROVAL
}
enum UserCreatedVia {
ADMIN
PUBLIC_SHIFT_SIGNUP
STANDARD
SELF_REGISTRATION
}
model User {
id String @id @default(cuid())
email String @unique
password String // bcrypt hashed
name String?
phone String?
role UserRole @default(USER)
roles Json @default("[]") // Array of UserRole strings for multi-role support
status UserStatus @default(ACTIVE)
permissions Json? // Per-app granular permissions
createdVia UserCreatedVia @default(STANDARD)
expiresAt DateTime? // For temp users
expireDays Int?
lastLoginAt DateTime?
emailVerified Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
refreshTokens RefreshToken[]
campaignsCreated Campaign[] @relation("CampaignCreator")
campaignsReviewed Campaign[] @relation("CampaignReviewer")
campaignEmails CampaignEmail[] @relation("CampaignEmailSender")
responses RepresentativeResponse[] @relation("ResponseSubmitter")
responseUpvotes ResponseUpvote[]
shiftSignups ShiftSignup[]
locationsCreated Location[] @relation("LocationCreator")
locationsUpdated Location[] @relation("LocationUpdater")
addressesCreated Address[] @relation("AddressCreator")
addressesUpdated Address[] @relation("AddressUpdater")
locationEdits LocationHistory[] @relation("LocationHistoryUser")
cutsCreated Cut[] @relation("CutCreator")
canvassVisits CanvassVisit[] @relation("CanvassVisitor")
canvassSessions CanvassSession[] @relation("CanvassSessions")
trackingSessions TrackingSession[] @relation("TrackingSessions")
templatesCreated EmailTemplate[] @relation("TemplatesCreated")
templatesUpdated EmailTemplate[] @relation("TemplatesUpdated")
templateVersionsCreated EmailTemplateVersion[] @relation("TemplateVersionsCreated")
templateTestsSent EmailTemplateTestLog[] @relation("TemplateTestsSent")
// Media API relations
videosUploaded Video[] @relation("VideoUploader")
videosLocked Video[] @relation("VideoLocker")
videoViews VideoView[] @relation("VideoViews")
videoScheduleHistory VideoScheduleHistory[] @relation("VideoScheduleHistory")
sessions Session[] @relation("SessionUser")
comments Comment[] @relation("CommentUser")
authTokens AuthToken[] @relation("AuthTokenUser")
sessionBansMade SessionBan[] @relation("SessionBanner")
commentModerations CommentModeration[] @relation("CommentModerator")
emailVerificationTokens EmailVerificationToken[] @relation("EmailVerificationTokens")
passwordResetTokens PasswordResetToken[] @relation("PasswordResetTokens")
emailChangeTokens EmailChangeToken[] @relation("EmailChangeTokens")
achievements UserAchievement[] @relation("UserAchievements")
stats UserStats? @relation("UserStats")
finishes UserFinish[] @relation("UserFinishes")
videoReactions VideoReaction[] @relation("VideoReactions")
highlightCooldowns HighlightCooldown[] @relation("HighlightCooldowns")
dailyActivity UserDailyActivity[] @relation("UserDailyActivity")
chatThreadReadStatus ChatThreadReadStatus[] @relation("ChatThreadReadStatus")
moderationWordLists ModerationWordList[] @relation("ModerationWordListCreator")
contentReportsSubmitted ContentReport[] @relation("ContentReportUser")
contentReportsResolved ContentReport[] @relation("ContentReportResolver")
playlists Playlist[] @relation("UserPlaylists")
featuredPlaylists FeaturedPlaylist[] @relation("FeaturedPlaylistFeaturer")
adImpressions AdImpression[] @relation("AdImpressionUser")
adClicks AdClick[] @relation("AdClickUser")
friendships Friendship[] @relation("UserFriendships")
friends Friendship[] @relation("UserFriends")
blocks UserBlock[] @relation("UserBlocks")
blockedBy UserBlock[] @relation("UserBlockedBy")
pokesSent Poke[] @relation("PokesSent")
pokesReceived Poke[] @relation("PokesReceived")
recommendationsSent VideoRecommendation[] @relation("RecommendationsSent")
recommendationsReceived VideoRecommendation[] @relation("RecommendationsReceived")
presence UserPresence? @relation("UserPresence")
galleryImages UserGalleryImage[] @relation("UserGalleryImages")
socialLinks UserSocialLink[] @relation("UserSocialLinks")
privacySettings PrivacySettings? @relation("PrivacySettings")
closeFriends CloseFriend[] @relation("CloseFriends")
closeFriendOf CloseFriend[] @relation("CloseFriendOf")
uploads UserUpload[] @relation("UserUploads")
uploadReviews UserUpload[] @relation("UserUploadReviewer")
uploadInvites UploadInvite[] @relation("UploadInviteCreator")
tagPreferences UserTagPreference[] @relation("UserTagPreferences")
performerDiscrepancies PerformerDiscrepancy[] @relation("PerformerDiscrepancyResolver")
watchPartiesHosted WatchPartySession[] @relation("WatchPartyHost")
watchPartyParticipations WatchPartyParticipant[] @relation("WatchPartyParticipant")
watchPartyChatMessages WatchPartyChatMessage[] @relation("WatchPartyChatUser")
watchPartyReactions WatchPartyReaction[] @relation("WatchPartyReactionUser")
watchPartyInvitesSent WatchPartyInvite[] @relation("WatchPartyInviter")
watchPartyInvitesReceived WatchPartyInvite[] @relation("WatchPartyInvitee")
subscriptions UserSubscription[] @relation("UserSubscriptions")
invoices Invoice[] @relation("UserInvoices")
payments Payment[] @relation("UserPayments")
paymentAudits PaymentAuditLog[] @relation("PaymentAuditUser")
orders Order[] @relation("UserOrders")
notifications Notification[] @relation("UserNotifications")
notificationPreferences NotificationPreferences? @relation("NotificationPreferences")
@@map("users")
}
model RefreshToken {
id String @id @default(cuid())
token String @unique
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
@@index([userId])
@@map("refresh_tokens")
}
// ============================================================================
// INFLUENCE — CAMPAIGNS
// ============================================================================
enum CampaignStatus {
DRAFT
ACTIVE
PAUSED
ARCHIVED
}
enum CampaignModerationStatus {
PENDING_REVIEW
APPROVED
REJECTED
CHANGES_REQUESTED
}
enum GovernmentLevel {
FEDERAL
PROVINCIAL
MUNICIPAL
SCHOOL_BOARD
}
model Campaign {
id String @id @default(cuid())
slug String @unique
title String
description String? @db.Text
emailSubject String
emailBody String @db.Text
callToAction String? @db.Text
coverPhoto String?
status CampaignStatus @default(DRAFT)
// Feature flags
allowSmtpEmail Boolean @default(true)
allowMailtoLink Boolean @default(true)
collectUserInfo Boolean @default(true)
showEmailCount Boolean @default(true)
showCallCount Boolean @default(true)
allowEmailEditing Boolean @default(false)
allowCustomRecipients Boolean @default(false)
showResponseWall Boolean @default(false)
highlightCampaign Boolean @default(false)
// Targeting
targetGovernmentLevels GovernmentLevel[]
// Creator
createdByUserId String?
createdByUser User? @relation("CampaignCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
createdByUserEmail String?
createdByUserName String?
// User-generated campaign moderation
isUserGenerated Boolean @default(false)
moderationStatus CampaignModerationStatus?
reviewedByUserId String?
reviewedByUser User? @relation("CampaignReviewer", fields: [reviewedByUserId], references: [id], onDelete: SetNull)
reviewedAt DateTime?
rejectionReason String? @db.Text
moderationNotes String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
emails CampaignEmail[]
responses RepresentativeResponse[]
customRecipients CustomRecipient[]
calls Call[]
@@index([moderationStatus])
@@index([isUserGenerated])
@@map("campaigns")
}
// ============================================================================
// INFLUENCE — REPRESENTATIVES
// ============================================================================
model Representative {
id String @id @default(cuid())
postalCode String
name String?
email String?
districtName String?
electedOffice String?
partyName String?
representativeSetName String?
url String?
photoUrl String?
offices Json? // JSON array of office contact info
cachedAt DateTime @default(now())
@@index([postalCode])
@@map("representatives")
}
// ============================================================================
// INFLUENCE — CAMPAIGN EMAILS
// ============================================================================
enum EmailMethod {
SMTP
MAILTO
}
enum CampaignEmailStatus {
QUEUED
SENT
FAILED
CLICKED
USER_INFO_CAPTURED
}
model CampaignEmail {
id String @id @default(cuid())
campaignId String
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
campaignSlug String
// Sender info
userId String?
user User? @relation("CampaignEmailSender", fields: [userId], references: [id], onDelete: SetNull)
userEmail String?
userName String?
userPostalCode String?
// Recipient info
recipientEmail String
recipientName String?
recipientTitle String?
recipientLevel GovernmentLevel?
emailMethod EmailMethod
subject String
message String @db.Text
status CampaignEmailStatus @default(SENT)
senderIp String?
sentAt DateTime @default(now())
@@index([campaignId])
@@index([campaignSlug])
@@index([userPostalCode])
@@index([sentAt])
@@map("campaign_emails")
}
// ============================================================================
// INFLUENCE — REPRESENTATIVE RESPONSES (Response Wall)
// ============================================================================
enum ResponseType {
EMAIL
LETTER
PHONE_CALL
MEETING
SOCIAL_MEDIA
OTHER
}
enum ResponseStatus {
PENDING
APPROVED
REJECTED
}
model RepresentativeResponse {
id String @id @default(cuid())
campaignId String
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
campaignSlug String
representativeName String
representativeTitle String?
representativeLevel GovernmentLevel
representativeEmail String?
responseType ResponseType
responseText String @db.Text
userComment String? @db.Text
screenshotUrl String?
// Submitter info
submittedByUserId String?
submittedByUser User? @relation("ResponseSubmitter", fields: [submittedByUserId], references: [id], onDelete: SetNull)
submittedByName String?
submittedByEmail String?
isAnonymous Boolean @default(false)
// Moderation
status ResponseStatus @default(PENDING)
// Verification
isVerified Boolean @default(false)
verificationToken String?
verificationSentAt DateTime?
verifiedAt DateTime?
verifiedBy String?
upvoteCount Int @default(0)
submittedIp String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
upvotes ResponseUpvote[]
@@index([campaignId])
@@index([campaignSlug])
@@index([representativeName])
@@map("representative_responses")
}
model ResponseUpvote {
id String @id @default(cuid())
responseId String
response RepresentativeResponse @relation(fields: [responseId], references: [id], onDelete: Cascade)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
userEmail String?
upvotedIp String?
@@unique([responseId, userId])
@@unique([responseId, upvotedIp])
@@map("response_upvotes")
}
// ============================================================================
// INFLUENCE — CUSTOM RECIPIENTS
// ============================================================================
model CustomRecipient {
id String @id @default(cuid())
campaignId String
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
campaignSlug String
recipientName String
recipientEmail String
recipientTitle String?
recipientOrganization String?
notes String? @db.Text
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([campaignId])
@@map("custom_recipients")
}
// ============================================================================
// INFLUENCE — POSTAL CODE CACHE
// ============================================================================
model PostalCodeCache {
id String @id @default(cuid())
postalCode String @unique
city String?
province String?
centroidLat Decimal? @db.Decimal(10, 8)
centroidLng Decimal? @db.Decimal(11, 8)
lastUpdated DateTime @default(now())
@@map("postal_code_cache")
}
// ============================================================================
// INFLUENCE — EMAIL LOG & VERIFICATION
// ============================================================================
model EmailLog {
id String @id @default(cuid())
recipientEmail String
senderName String
senderEmail String
subject String?
message String? @db.Text
postalCode String?
status String @default("sent") // sent, failed, previewed
senderIp String?
sentAt DateTime @default(now())
@@map("email_logs")
}
model EmailVerification {
id String @id @default(cuid())
token String @unique
email String
tempCampaignData String? @db.Text // JSON
createdAt DateTime @default(now())
expiresAt DateTime
used Boolean @default(false)
@@map("email_verifications")
}
// ============================================================================
// INFLUENCE — CALL TRACKING
// ============================================================================
model Call {
id String @id @default(cuid())
representativeName String
representativeTitle String?
phoneNumber String
officeType String?
callerName String?
callerEmail String?
postalCode String?
campaignId String?
campaign Campaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull)
campaignSlug String?
callerIp String?
calledAt DateTime @default(now())
@@index([campaignId])
@@map("calls")
}
// ============================================================================
// MAP — LOCATIONS
// ============================================================================
enum SupportLevel {
LEVEL_1 @map("1")
LEVEL_2 @map("2")
LEVEL_3 @map("3")
LEVEL_4 @map("4")
}
enum GeocodeProvider {
GOOGLE
MAPBOX
NOMINATIM
PHOTON
LOCATIONIQ
ARCGIS
UNKNOWN
}
enum BuildingType {
SINGLE_FAMILY
MULTI_UNIT
MIXED_USE
COMMERCIAL
}
model Location {
id String @id @default(cuid())
latitude Decimal @db.Decimal(10, 8) // Required (was nullable)
longitude Decimal @db.Decimal(11, 8) // Required (was nullable)
// Building-level data
address String // Base street address (no unit number)
postalCode String?
province String?
federalDistrict String?
buildingUse Int? // NAR BU_USE: 1=Residential, 2=Partial, 3=Non-Residential, 4=Unknown
// NAR + building metadata
locGuid String? @unique
buildingType BuildingType @default(SINGLE_FAMILY)
totalUnits Int @default(1)
buildingNotes String? @db.Text // Access codes, manager contact, etc.
// Geocoding
geocodeConfidence Int? // 0-100
geocodeProvider GeocodeProvider?
// Audit
createdByUserId String?
createdByUser User? @relation("LocationCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
updatedByUserId String?
updatedByUser User? @relation("LocationUpdater", fields: [updatedByUserId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
addresses Address[]
history LocationHistory[]
@@index([latitude, longitude])
@@index([latitude])
@@index([longitude])
@@index([postalCode])
@@map("locations")
}
model Address {
id String @id @default(cuid())
locationId String
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
// Unit identification
unitNumber String?
addrGuid String? @unique // NAR ADDR_GUID
// Occupant/contact info (per-unit)
firstName String?
lastName String?
email String?
phone String?
// Canvassing data (per-unit)
supportLevel SupportLevel?
sign Boolean @default(false)
signSize String?
notes String? @db.Text
// Audit
createdByUserId String?
createdByUser User? @relation("AddressCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
updatedByUserId String?
updatedByUser User? @relation("AddressUpdater", fields: [updatedByUserId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
canvassVisits CanvassVisit[]
@@index([locationId])
@@index([locationId, id])
@@index([locationId, unitNumber])
@@map("addresses")
}
// ============================================================================
// MAP — LOCATION HISTORY
// ============================================================================
enum LocationHistoryAction {
CREATED
UPDATED
GEOCODED
BULK_GEOCODED
MOVED_ON_MAP
IMPORTED_CSV
IMPORTED_NAR
}
model LocationHistory {
id String @id @default(cuid())
locationId String
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
userId String?
user User? @relation("LocationHistoryUser", fields: [userId], references: [id], onDelete: SetNull)
action LocationHistoryAction
field String? // Which field changed
oldValue String? @db.Text
newValue String? @db.Text
metadata Json? // Provider, confidence, etc.
createdAt DateTime @default(now())
@@index([locationId])
@@index([userId])
@@index([createdAt])
@@map("location_history")
}
// ============================================================================
// MAP — SHIFTS
// ============================================================================
enum ShiftStatus {
OPEN
FULL
CANCELLED
}
enum RecurrenceFrequency {
DAILY
WEEKLY
MONTHLY
}
model Shift {
id String @id @default(cuid())
title String
description String? @db.Text
date DateTime @db.Date
startTime String // HH:MM format
endTime String // HH:MM format
location String?
maxVolunteers Int
currentVolunteers Int @default(0)
status ShiftStatus @default(OPEN)
isPublic Boolean @default(false)
cutId String?
cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull)
// Repeating shift series
seriesId String?
series ShiftSeries? @relation(fields: [seriesId], references: [id], onDelete: SetNull)
isException Boolean @default(false)
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
signups ShiftSignup[]
canvassVisits CanvassVisit[]
canvassSessions CanvassSession[]
@@index([cutId])
@@index([seriesId])
@@map("shifts")
}
enum SignupStatus {
CONFIRMED
CANCELLED
}
enum SignupSource {
AUTHENTICATED
PUBLIC
ADMIN
}
model ShiftSignup {
id String @id @default(cuid())
shiftId String
shift Shift @relation(fields: [shiftId], references: [id], onDelete: Cascade)
shiftTitle String?
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
userEmail String
userName String?
userPhone String?
signupDate DateTime @default(now())
status SignupStatus @default(CONFIRMED)
signupSource SignupSource @default(AUTHENTICATED)
@@unique([shiftId, userEmail])
@@index([shiftId])
@@map("shift_signups")
}
// ============================================================================
// MAP — SHIFT SERIES (Repeating Shifts)
// ============================================================================
model ShiftSeries {
id String @id @default(cuid())
title String
description String? @db.Text
startTime String // HH:MM format
endTime String // HH:MM format
location String?
maxVolunteers Int
isPublic Boolean @default(false)
cutId String?
cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull)
// Recurrence rules
frequency RecurrenceFrequency
daysOfWeek Json? // Array of day numbers: [1,3,5] for Mon/Wed/Fri
startDate DateTime @db.Date
endDate DateTime? @db.Date
// Metadata
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
shifts Shift[]
@@index([cutId])
@@map("shift_series")
}
// ============================================================================
// MAP — CUTS (Geographic Polygon Overlays)
// ============================================================================
enum CutCategory {
CUSTOM
WARD
NEIGHBORHOOD
DISTRICT
}
model Cut {
id String @id @default(cuid())
name String
description String? @db.Text
color String @default("#3388ff")
opacity Decimal @default(0.3) @db.Decimal(3, 2)
category CutCategory?
isPublic Boolean @default(false)
isOfficial Boolean @default(false)
geojson String @db.Text // GeoJSON polygon data
bounds String? @db.Text // Bounding box JSON
showLocations Boolean @default(true)
exportEnabled Boolean @default(true)
assignedTo String?
filterSettings Json? // JSON filter configuration
lastCanvassed DateTime?
completionPercentage Int @default(0)
createdByUserId String?
createdByUser User? @relation("CutCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
shifts Shift[]
shiftSeries ShiftSeries[]
canvassSessions CanvassSession[]
@@map("cuts")
}
// ============================================================================
// MAP — SETTINGS
// ============================================================================
model MapSettings {
id String @id @default(cuid())
latitude Decimal? @db.Decimal(10, 8)
longitude Decimal? @db.Decimal(11, 8)
zoom Int?
walkSheetTitle String?
walkSheetSubtitle String?
walkSheetFooter String? @db.Text
qrCode1Url String?
qrCode1Label String?
qrCode2Url String?
qrCode2Label String?
qrCode3Url String?
qrCode3Label String?
publicMapEnabled Boolean @default(true)
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("map_settings")
}
// ============================================================================
// SITE SETTINGS (Singleton — branding, theme, feature toggles)
// ============================================================================
model SiteSettings {
id String @id @default(cuid())
// Organization
organizationName String @default("Changemaker Lite")
organizationShortName String @default("CML")
organizationLogoUrl String?
organizationFaviconUrl String?
// Admin theme
adminColorPrimary String @default("#9d4edd")
adminColorBgBase String @default("#1a1025")
// Public theme
publicColorPrimary String @default("#3498db")
publicColorBgBase String @default("#0d1b2a")
publicColorBgContainer String @default("#1b2838")
publicHeaderGradient String @default("linear-gradient(135deg, #005a9c 0%, #007acc 100%)")
// Text
footerText String @default("Powered by Changemaker Lite")
loginSubtitle String @default("Admin")
// Email branding
emailFromName String @default("Changemaker Lite")
// SMTP configuration (overrides env vars when set; empty/0 = use env fallback)
smtpHost String @default("")
smtpPort Int @default(0)
smtpUser String @default("")
smtpPass String @default("")
smtpFromAddress String @default("")
smtpActiveProvider String @default("mailhog") // "mailhog" | "production"
emailTestMode Boolean @default(true)
testEmailRecipient String @default("")
// Registration settings
enablePublicRegistration Boolean @default(true)
enableEmailVerification Boolean @default(true)
autoApproveVerifiedUsers Boolean @default(true)
// Feature toggles
enableInfluence Boolean @default(true)
enableMap Boolean @default(true)
enableNewsletter Boolean @default(true)
enableLandingPages Boolean @default(true)
enableMediaFeatures Boolean @default(true) @map("enable_media_features")
enablePayments Boolean @default(false)
enableGalleryAds Boolean @default(false) @map("enable_gallery_ads")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("site_settings")
}
// ============================================================================
// EMAIL TEMPLATES
// ============================================================================
enum EmailTemplateCategory {
INFLUENCE
MAP
SYSTEM
PAYMENT
}
enum EmailTemplateVariableType {
TEXT
VIDEO
}
model EmailTemplate {
id String @id @default(cuid())
key String @unique // e.g., "campaign-email"
name String // Display name
description String? @db.Text
category EmailTemplateCategory // INFLUENCE | MAP | SYSTEM
subjectLine String // Template with {{VAR}} support
htmlContent String @db.Text
textContent String @db.Text
isSystem Boolean @default(false) // Prevent deletion
isActive Boolean @default(true)
variables EmailTemplateVariable[]
versions EmailTemplateVersion[]
testLogs EmailTemplateTestLog[]
createdByUserId String
createdBy User @relation("TemplatesCreated", fields: [createdByUserId], references: [id])
updatedByUserId String?
updatedBy User? @relation("TemplatesUpdated", fields: [updatedByUserId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([category])
@@index([isActive])
@@map("email_templates")
}
model EmailTemplateVariable {
id String @id @default(cuid())
templateId String
template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
key String // e.g., "USER_NAME"
label String // e.g., "User Name"
description String? @db.Text // e.g., "Name of the user sending the email"
type EmailTemplateVariableType @default(TEXT) // TEXT | VIDEO
videoId Int? // Optional FK to videos (not enforced, separate Media API DB)
isRequired Boolean @default(true)
isConditional Boolean @default(false) // Used in {{#if}} blocks
sampleValue String? @db.Text // e.g., "John Doe"
sortOrder Int @default(0)
@@unique([templateId, key])
@@index([templateId])
@@index([type])
@@map("email_template_variables")
}
model EmailTemplateVersion {
id String @id @default(cuid())
templateId String
template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
versionNumber Int // Auto-increment per template
subjectLine String
htmlContent String @db.Text
textContent String @db.Text
changeNotes String? @db.Text
createdByUserId String
createdBy User @relation("TemplateVersionsCreated", fields: [createdByUserId], references: [id])
createdAt DateTime @default(now())
@@unique([templateId, versionNumber])
@@index([templateId, createdAt(sort: Desc)])
@@map("email_template_versions")
}
model EmailTemplateTestLog {
id String @id @default(cuid())
templateId String
template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
recipientEmail String
testData Json // Sample variable values used
success Boolean
errorMessage String? @db.Text
messageId String? // Nodemailer message ID
sentByUserId String
sentBy User @relation("TemplateTestsSent", fields: [sentByUserId], references: [id])
sentAt DateTime @default(now())
@@index([templateId, sentAt(sort: Desc)])
@@map("email_template_test_logs")
}
// ============================================================================
// LANDING PAGES
// ============================================================================
enum EditorMode {
VISUAL
CODE
}
enum MkdocsExportMode {
THEMED // extends main.html, content block only
STANDALONE // full HTML document, no Jinja2 inheritance
}
model LandingPage {
id String @id @default(cuid())
slug String @unique
title String
description String? @db.Text
blocks Json // JSON from GrapesJS editor
htmlOutput String? @db.Text
cssOutput String? @db.Text
editorMode EditorMode @default(VISUAL)
mkdocsPath String? // Path in mkdocs/overrides/
mkdocsStubPath String? // Path to .md stub in mkdocs/docs/
mkdocsExportMode MkdocsExportMode @default(THEMED)
mkdocsHideNav Boolean @default(true)
mkdocsHideToc Boolean @default(true)
mkdocsSkipExport Boolean @default(false)
published Boolean @default(false)
seoTitle String?
seoDescription String? @db.Text
seoImage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("landing_pages")
}
model PageBlock {
id String @id @default(cuid())
type String // hero, text, image, cta, features, testimonials, form
label String
schema Json // Block configuration schema
defaults Json // Default values
thumbnail String?
category String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("page_blocks")
}
// ============================================================================
// MAP — CANVASSING
// ============================================================================
enum VisitOutcome {
NOT_HOME
REFUSED
MOVED
ALREADY_VOTED
SPOKE_WITH
LEFT_LITERATURE
COME_BACK_LATER
}
enum CanvassSessionStatus {
ACTIVE
COMPLETED
ABANDONED
}
model CanvassSession {
id String @id @default(cuid())
userId String
user User @relation("CanvassSessions", fields: [userId], references: [id], onDelete: Cascade)
cutId String
cut Cut @relation(fields: [cutId], references: [id], onDelete: Cascade)
shiftId String?
shift Shift? @relation(fields: [shiftId], references: [id], onDelete: SetNull)
status CanvassSessionStatus @default(ACTIVE)
startedAt DateTime @default(now())
endedAt DateTime?
startLatitude Decimal? @db.Decimal(10, 8)
startLongitude Decimal? @db.Decimal(11, 8)
visits CanvassVisit[]
trackingSession TrackingSession?
@@index([userId])
@@index([cutId])
@@index([shiftId])
@@map("canvass_sessions")
}
model CanvassVisit {
id String @id @default(cuid())
addressId String // Changed from locationId
address Address @relation(fields: [addressId], references: [id], onDelete: Cascade)
userId String
user User @relation("CanvassVisitor", fields: [userId], references: [id], onDelete: Cascade)
shiftId String?
shift Shift? @relation(fields: [shiftId], references: [id], onDelete: SetNull)
sessionId String?
session CanvassSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
outcome VisitOutcome
supportLevel SupportLevel?
signRequested Boolean @default(false)
signSize String?
notes String? @db.Text
durationSeconds Int?
visitedAt DateTime @default(now())
@@index([addressId]) // Changed from locationId
@@index([addressId, visitedAt(sort: Desc)]) // For distinct + orderBy queries
@@index([userId])
@@index([shiftId])
@@index([sessionId])
@@index([visitedAt])
@@map("canvass_visits")
}
// ============================================================================
// MAP — GPS TRACKING
// ============================================================================
enum TrackPointEvent {
LOCATION_ADDED
VISIT_RECORDED
SESSION_STARTED
SESSION_ENDED
}
model TrackingSession {
id String @id @default(cuid())
userId String
user User @relation("TrackingSessions", fields: [userId], references: [id], onDelete: Cascade)
canvassSessionId String? @unique
canvassSession CanvassSession? @relation(fields: [canvassSessionId], references: [id], onDelete: SetNull)
startedAt DateTime @default(now())
endedAt DateTime?
isActive Boolean @default(true)
totalPoints Int @default(0)
totalDistanceM Float @default(0)
lastLatitude Decimal? @db.Decimal(10, 8)
lastLongitude Decimal? @db.Decimal(11, 8)
lastRecordedAt DateTime?
trackPoints TrackPoint[]
@@index([userId])
@@index([isActive])
@@index([isActive, lastRecordedAt])
@@map("tracking_sessions")
}
model TrackPoint {
id String @id @default(cuid())
trackingSessionId String
trackingSession TrackingSession @relation(fields: [trackingSessionId], references: [id], onDelete: Cascade)
latitude Decimal @db.Decimal(10, 8)
longitude Decimal @db.Decimal(11, 8)
accuracy Float?
recordedAt DateTime @default(now())
eventType TrackPointEvent?
@@index([trackingSessionId, recordedAt])
@@index([recordedAt])
@@map("track_points")
}
// Enums
enum DirectoryType {
studios
gifs
private
inbox
curated
playback
compilations
videos
highlights
}
enum ResourceCategory {
gpu_ai
gpu_encode
cpu
}
enum JobStatus {
pending
queued
running
completed
failed
cancelled
}
enum ReactionType {
like
love
laugh
wow
sad
angry
}
enum WordFilterLevel {
low
medium
high
custom
}
enum ReportType {
inappropriate
spam
copyright
illegal
false_info
other
}
enum ReportStatus {
pending
reviewed
actioned
dismissed
}
enum FriendshipStatus {
pending
accepted
declined
}
enum SocialPlatform {
twitter
instagram
onlyfans
fansly
reddit
discord
tiktok
youtube
twitch
snapchat
linktree
custom
}
enum UserUploadStatus {
pending
approved
rejected
}
enum UploadInviteStatus {
active
inactive
expired
}
enum DigestStatus {
pending
scene_detection
extracting
analyzing
face_detection
transcribing
segmenting
synthesizing
completed
failed
cancelled
}
enum ClipType {
hook
intro
action
climax
highlight
}
enum ClipStatus {
pending
processing
completed
failed
}
enum ClipSource {
machine
manual
}
enum CaptionPosition {
bottom
top
}
enum CaptionSize {
small
medium
large
}
enum SegmentType {
scene
tag_change
vocal
fixed_interval
}
enum VocalCategory {
dialogue
dirty_talk
climax
interview
moan
}
enum SuggestedTagStatus {
pending
approved
rejected
mapped
}
enum WatchPartyStatus {
active
completed
cancelled
}
enum PipelineStatus {
draft
active
paused
completed
failed
}
enum PipelineStepStatus {
pending
running
completed
failed
skipped
}
enum SubscriptionStatus {
none
active
grace_period
delinquent
lifetime
cancelled
}
enum InvoiceStatus {
pending
paid
failed
refunded
}
enum PaymentStatus {
pending
succeeded
failed
refunded
}
enum PaymentMethod {
card
bank_transfer
crypto
stripe
}
enum ProductType {
DIGITAL
EVENT
DONATION
}
enum OrderStatus {
PENDING
COMPLETED
FAILED
REFUNDED
}
enum NotificationType {
friend_request
friend_accepted
poke
comment
upload_approved
upload_rejected
achievement
system
}
// ============================================================================
// CORE VIDEO LIBRARY
// ============================================================================
model Video {
id Int @id @default(autoincrement())
path String @unique
filename String
producer String?
creator String?
title String?
durationSeconds Int? @map("duration_seconds")
quality String?
orientation String?
hasAudio Boolean? @default(true) @map("has_audio")
fileSize BigInt? @map("file_size")
fileHash String? @map("file_hash")
width Int?
height Int?
lastValidated DateTime? @map("last_validated")
isValid Boolean? @default(true) @map("is_valid")
thumbnailPath String? @map("thumbnail_path")
createdAt DateTime @default(now()) @map("created_at")
tags Json?
directoryType DirectoryType? @map("directory_type")
// Historical engagement stats
publicViewCount Int? @map("public_view_count")
publicUpvoteCount Int? @map("public_upvote_count")
publicCommentCount Int? @map("public_comment_count")
publicCompletionCount Int? @map("public_completion_count")
publicTotalWatchTime Int? @map("public_total_watch_time")
movedFromPublicAt DateTime? @map("moved_from_public_at")
// Name standardization tracking
originalFilename String? @map("original_filename")
originalPath String? @map("original_path")
standardizedAt DateTime? @map("standardized_at")
// Publishing system (replaces PublicMedia)
isPublished Boolean @default(false) @map("is_published")
publishedAt DateTime? @map("published_at")
category String? // videos|curated|compilations|playback|highlights
isShort Boolean @default(false) @map("is_short")
// Moderation system
isLocked Boolean @default(false) @map("is_locked")
lockedAt DateTime? @map("locked_at")
lockedById String? @map("locked_by_id")
// Engagement counters
viewCount Int @default(0) @map("view_count")
upvoteCount Int @default(0) @map("upvote_count")
commentCount Int @default(0) @map("comment_count")
finishCount Int @default(0) @map("finish_count")
totalWatchTime Int @default(0) @map("total_watch_time")
// Scheduled publishing
scheduledPublishAt DateTime? @map("scheduled_publish_at")
scheduledUnpublishAt DateTime? @map("scheduled_unpublish_at")
// Enhanced analytics
uniqueViewers Int @default(0) @map("unique_viewers")
totalWatchTimeSeconds Int @default(0) @map("total_watch_time_seconds")
averageWatchTimeSeconds Decimal @default(0) @map("average_watch_time_seconds") @db.Decimal(10, 2)
completionRate Decimal @default(0) @map("completion_rate") @db.Decimal(5, 2)
// Content gating
accessLevel String @default("free") @map("access_level") // free|member|premium
// Ordering
position Int? @default(0)
// Uploader tracking
uploaderId String? @map("uploader_id")
// Relations
uploader User? @relation("VideoUploader", fields: [uploaderId], references: [id])
locker User? @relation("VideoLocker", fields: [lockedById], references: [id])
upvotes Upvote[]
comments Comment[]
views View[]
reactions VideoReaction[]
finishes UserFinish[]
contentReports ContentReport[]
playlistVideos PlaylistVideo[]
publicMediaTags PublicMediaTag[] @relation("PublicMediaTags")
publicMediaPerformers PublicMediaPerformer[] @relation("PublicMediaPerformers")
recommendations VideoRecommendation[]
videoDigests VideoDigest[]
digestVideoTags DigestVideoTag[]
digestSelectedClips DigestSelectedClip[]
digestGeneratedScenes DigestGeneratedScene[]
digestOutputFolders DigestOutputFolder[]
digestCompilations DigestCompilation[]
videoSceneCuts VideoSceneCut[]
videoTagTimeline VideoTagTimeline[]
videoSegments VideoSegment[]
videoTags VideoTag[]
digestClipTags DigestClipTag[]
tagGenerationJobs TagGenerationJob[]
videoOcrResults VideoOcrResult[]
videoViews VideoView[]
videoEvents VideoEvent[]
scheduleHistory VideoScheduleHistory[]
@@index([orientation], map: "idx_orientation")
@@index([producer], map: "idx_producer")
@@index([isValid], map: "idx_is_valid")
@@index([directoryType], map: "idx_directory_type")
@@index([durationSeconds, fileSize, width, height], map: "idx_videos_fingerprint")
@@index([directoryType, isValid, orientation], map: "idx_videos_directory_valid_orientation")
@@index([isPublished, isLocked], map: "idx_videos_published_locked")
@@index([category, isPublished], map: "idx_videos_category_published")
@@index([isShort, isPublished, isLocked], map: "idx_videos_short_published")
@@index([uploaderId], map: "idx_videos_uploader")
@@map("videos")
}
model Compilation {
id Int @id @default(autoincrement())
filename String
path String?
durationSeconds Int? @map("duration_seconds")
videoIds Json? @map("video_ids")
settings Json?
createdAt DateTime @default(now()) @map("created_at")
@@map("compilations")
}
model Job {
id Int @id @default(autoincrement())
type String
status JobStatus? @default(pending)
progress Int? @default(0)
log String?
params Json?
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
// Queue management
resourceCategory ResourceCategory? @default(cpu) @map("resource_category")
vramRequired Int? @default(0) @map("vram_required")
queuePosition Int? @map("queue_position")
waitingReason String? @map("waiting_reason")
priority Int? @default(5)
// Pipeline integration
pipelineId Int? @map("pipeline_id")
pipelineStepId Int? @map("pipeline_step_id")
// Relations
pipeline Pipeline? @relation(fields: [pipelineId], references: [id])
pipelineStep PipelineStep? @relation(fields: [pipelineStepId], references: [id])
videoDigest VideoDigest?
tagGenerationJobs TagGenerationJob[]
@@index([status, priority, createdAt], map: "idx_jobs_queue")
@@index([resourceCategory, status], map: "idx_jobs_resource")
@@index([pipelineId], map: "idx_jobs_pipeline")
@@map("jobs")
}
// ============================================================================
// PUBLIC GALLERY (DEPRECATED - Consolidated into Video model)
// ============================================================================
// NOTE: PublicMedia model has been removed - all functionality consolidated into Video model
// with isPublished, category, isLocked, and engagement counter fields.
// Migration path: All existing PublicMedia data should be migrated to Video table with
// isPublished=true before dropping this table.
// COMMENTED OUT - SCHEDULED FOR DELETION AFTER DATA MIGRATION
// model PublicMedia {
// id Int @id @default(autoincrement())
// path String @unique
// filename String
// category String
// durationSeconds Int? @map("duration_seconds")
// quality String?
// orientation String?
// thumbnailPath String? @map("thumbnail_path")
// fileSize BigInt? @map("file_size")
// viewCount Int? @default(0) @map("view_count")
// upvoteCount Int? @default(0) @map("upvote_count")
// commentCount Int? @default(0) @map("comment_count")
// finishCount Int? @default(0) @map("finish_count")
// totalWatchTime Int? @default(0) @map("total_watch_time")
// createdAt DateTime @default(now()) @map("created_at")
// isLocked Boolean? @default(false) @map("is_locked")
// lockedAt DateTime? @map("locked_at")
// lockedBy String? @map("locked_by")
// position Int? @default(0)
// uploaderId String? @map("uploader_id")
//
// @@index([category], map: "idx_public_media_category")
// @@index([path], map: "idx_public_media_path")
// @@index([isLocked], map: "idx_public_media_is_locked")
// @@index([position], map: "idx_public_media_position")
// @@index([uploaderId], map: "idx_public_media_uploader")
// @@index([category, createdAt], map: "idx_public_media_category_date")
// @@index([orientation], map: "idx_public_media_orientation")
// @@index([category, isLocked, createdAt], map: "idx_public_media_category_locked_date")
// @@map("public_media")
// }
model Session {
id String @id
createdAt DateTime @default(now()) @map("created_at")
lastSeenAt DateTime? @map("last_seen_at")
// Device tracking
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
deviceType String? @map("device_type")
browser String?
os String?
// Geography
country String?
countryName String? @map("country_name")
region String?
city String?
timezone String?
latitude Float? @db.Real
longitude Float? @db.Real
// User correlation
userId String? @map("user_id")
// Analytics
firstSeenAt DateTime? @map("first_seen_at")
visitCount Int? @default(1) @map("visit_count")
// Relations
user User? @relation("SessionUser", fields: [userId], references: [id])
upvotes Upvote[]
comments Comment[]
views View[]
sessionBans SessionBan[]
contentReports ContentReport[]
playlistViews PlaylistView[]
adImpressions AdImpression[]
adClicks AdClick[]
userFinishes UserFinish[]
@@index([userId], map: "idx_sessions_user_id")
@@index([country], map: "idx_sessions_country")
@@map("sessions")
}
model Upvote {
id Int @id @default(autoincrement())
mediaId Int @map("media_id")
sessionId String @map("session_id")
createdAt DateTime @default(now()) @map("created_at")
// Relations
media Video @relation(fields: [mediaId], references: [id])
session Session @relation(fields: [sessionId], references: [id])
@@index([mediaId, sessionId], map: "idx_upvotes_unique")
@@index([mediaId], map: "idx_upvotes_media")
@@map("upvotes")
}
model Comment {
id Int @id @default(autoincrement())
mediaId Int @map("media_id")
sessionId String @map("session_id")
userId String? @map("user_id")
content String
createdAt DateTime @default(now()) @map("created_at")
// Content safety fields
safetyStatus String? @default("pending") @map("safety_status")
safetyCheckedAt DateTime? @map("safety_checked_at")
safetyCategories Json? @map("safety_categories")
safetyReasoning String? @map("safety_reasoning")
// Hidden comment fields
isHidden Boolean? @default(false) @map("is_hidden")
hiddenAt DateTime? @map("hidden_at")
hiddenReason String? @map("hidden_reason")
moderationNotes String? @map("moderation_notes")
// Relations
media Video @relation(fields: [mediaId], references: [id])
session Session @relation(fields: [sessionId], references: [id])
user User? @relation("CommentUser", fields: [userId], references: [id])
moderation CommentModeration?
watchPartyMessages WatchPartyChatMessage[]
@@index([mediaId], map: "idx_comments_media")
@@index([sessionId], map: "idx_comments_session")
@@index([userId], map: "idx_comments_user")
@@index([safetyStatus], map: "idx_comments_safety_status")
@@index([isHidden], map: "idx_comments_is_hidden")
@@map("comments")
}
model View {
id Int @id @default(autoincrement())
mediaId Int @map("media_id")
sessionId String @map("session_id")
watchTimeSeconds Int? @default(0) @map("watch_time_seconds")
lastUpdated DateTime @default(now()) @map("last_updated")
createdAt DateTime @default(now()) @map("created_at")
// Relations
media Video @relation(fields: [mediaId], references: [id])
session Session @relation(fields: [sessionId], references: [id])
@@index([mediaId, sessionId], map: "idx_views_unique")
@@index([mediaId], map: "idx_views_media")
@@map("views")
}
// ============================================================================
// USER MANAGEMENT (EXCLUDING v2Users and mediaUsers)
// ============================================================================
model AuthToken {
id Int @id @default(autoincrement())
userId String @map("user_id")
token String @unique
type String
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
// Relations
user User @relation("AuthTokenUser", fields: [userId], references: [id])
@@index([token], map: "idx_auth_tokens_token")
@@index([userId], map: "idx_auth_tokens_user")
@@map("auth_tokens")
}
model SessionBan {
id Int @id @default(autoincrement())
sessionId String @map("session_id")
reason String?
bannedBy String? @map("banned_by")
createdAt DateTime @default(now()) @map("created_at")
expiresAt DateTime? @map("expires_at")
// Relations
session Session @relation(fields: [sessionId], references: [id])
banner User? @relation("SessionBanner", fields: [bannedBy], references: [id])
@@index([sessionId], map: "idx_session_bans_session")
@@map("session_bans")
}
model CommentModeration {
id Int @id @default(autoincrement())
commentId Int @unique @map("comment_id")
status String @default("pending")
moderatedBy String? @map("moderated_by")
moderatedAt DateTime? @map("moderated_at")
reason String?
// Relations
comment Comment @relation(fields: [commentId], references: [id])
moderator User? @relation("CommentModerator", fields: [moderatedBy], references: [id])
@@index([commentId], map: "idx_comment_moderation_comment")
@@index([status], map: "idx_comment_moderation_status")
@@map("comment_moderation")
}
model EmailVerificationToken {
id Int @id @default(autoincrement())
userId String @map("user_id")
token String @unique
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
// Relations
user User @relation("EmailVerificationTokens", fields: [userId], references: [id])
@@index([token], map: "idx_email_verification_tokens_token")
@@index([userId], map: "idx_email_verification_tokens_user")
@@map("email_verification_tokens")
}
model PasswordResetToken {
id Int @id @default(autoincrement())
userId String @map("user_id")
token String @unique
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
usedAt DateTime? @map("used_at")
// Relations
user User @relation("PasswordResetTokens", fields: [userId], references: [id])
@@index([token], map: "idx_password_reset_tokens_token")
@@index([userId], map: "idx_password_reset_tokens_user")
@@map("password_reset_tokens")
}
model EmailChangeToken {
id Int @id @default(autoincrement())
userId String @map("user_id")
newEmail String @map("new_email")
token String @unique
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
// Relations
user User @relation("EmailChangeTokens", fields: [userId], references: [id])
@@index([token], map: "idx_email_change_tokens_token")
@@index([userId], map: "idx_email_change_tokens_user")
@@map("email_change_tokens")
}
// ============================================================================
// ACHIEVEMENTS & DASHBOARD
// ============================================================================
model UserAchievement {
id Int @id @default(autoincrement())
userId String @map("user_id")
achievementId String @map("achievement_id")
unlockedAt DateTime @map("unlocked_at")
progress Int? @default(0)
notified Boolean? @default(false)
// Relations
user User @relation("UserAchievements", fields: [userId], references: [id])
@@unique([userId, achievementId], map: "idx_user_achievements_unique")
@@index([userId], map: "idx_user_achievements_user")
@@map("user_achievements")
}
model UserStats {
id Int @id @default(autoincrement())
userId String @unique @map("user_id")
totalWatchTimeSeconds Int? @default(0) @map("total_watch_time_seconds")
totalVideosWatched Int? @default(0) @map("total_videos_watched")
totalUpvotesGiven Int? @default(0) @map("total_upvotes_given")
totalCommentsMade Int? @default(0) @map("total_comments_made")
totalFinishes Int? @default(0) @map("total_finishes")
currentDayStreak Int? @default(0) @map("current_day_streak")
longestDayStreak Int? @default(0) @map("longest_day_streak")
lastActiveDate String? @map("last_active_date")
longestSingleSession Int? @default(0) @map("longest_single_session")
categoriesCompleted Json? @map("categories_completed")
nightOwlCount Int? @default(0) @map("night_owl_count")
earlyBirdCount Int? @default(0) @map("early_bird_count")
updatedAt DateTime? @map("updated_at")
// Relations
user User @relation("UserStats", fields: [userId], references: [id])
@@map("user_stats")
}
model UserFinish {
id Int @id @default(autoincrement())
userId String @map("user_id")
mediaId Int? @map("media_id")
sessionId String? @map("session_id")
createdAt DateTime @map("created_at")
// Relations
user User @relation("UserFinishes", fields: [userId], references: [id])
media Video? @relation(fields: [mediaId], references: [id])
session Session? @relation(fields: [sessionId], references: [id])
@@index([userId], map: "idx_user_finishes_user")
@@index([createdAt], map: "idx_user_finishes_date")
@@map("user_finishes")
}
model VideoReaction {
id Int @id @default(autoincrement())
userId String @map("user_id")
mediaId Int @map("media_id")
reactionType ReactionType @map("reaction_type")
videoTimestamp Int @map("video_timestamp")
createdAt DateTime @default(now()) @map("created_at")
// Relations
user User @relation("VideoReactions", fields: [userId], references: [id])
media Video @relation(fields: [mediaId], references: [id])
@@index([userId, mediaId, reactionType], map: "idx_video_reactions_user_media_type")
@@index([mediaId, videoTimestamp], map: "idx_video_reactions_media_timestamp")
@@index([mediaId], map: "idx_video_reactions_media")
@@index([createdAt], map: "idx_video_reactions_created")
@@map("video_reactions")
}
model HighlightCooldown {
id Int @id @default(autoincrement())
userId String @map("user_id")
lastGeneratedAt DateTime @map("last_generated_at")
// Relations
user User @relation("HighlightCooldowns", fields: [userId], references: [id])
@@index([userId], map: "idx_highlight_cooldowns_user")
@@map("highlight_cooldowns")
}
model UserDailyActivity {
id Int @id @default(autoincrement())
userId String @map("user_id")
activityDate String @map("activity_date")
watchTimeSeconds Int? @default(0) @map("watch_time_seconds")
videosWatched Int? @default(0) @map("videos_watched")
firstActivityHour Int? @map("first_activity_hour")
createdAt DateTime? @map("created_at")
// Relations
user User @relation("UserDailyActivity", fields: [userId], references: [id])
@@index([userId, activityDate], map: "idx_user_daily_activity_unique")
@@map("user_daily_activity")
}
model ChatThreadReadStatus {
id Int @id @default(autoincrement())
userId String @map("user_id")
mediaId Int @map("media_id")
lastSeenAt DateTime @map("last_seen_at")
// Relations
user User @relation("ChatThreadReadStatus", fields: [userId], references: [id])
@@index([userId, mediaId], map: "idx_chat_thread_read_unique")
@@index([userId], map: "idx_chat_thread_read_user")
@@map("chat_thread_read_status")
}
// ============================================================================
// APP SETTINGS & MODERATION
// ============================================================================
model AppSetting {
key String @id
value String
updatedAt DateTime @default(now()) @map("updated_at")
@@map("app_settings")
}
model RateLimit {
id Int @id @default(autoincrement())
key String @unique
count Int @default(1)
resetAt DateTime @map("reset_at")
@@unique([key], map: "idx_rate_limits_key")
@@index([resetAt], map: "idx_rate_limits_reset_at")
@@map("rate_limits")
}
model ModerationWordList {
id Int @id @default(autoincrement())
level WordFilterLevel
word String
createdAt DateTime @default(now()) @map("created_at")
createdBy String? @map("created_by")
// Relations
creator User? @relation("ModerationWordListCreator", fields: [createdBy], references: [id])
@@index([level], map: "idx_moderation_word_lists_level")
@@index([word], map: "idx_moderation_word_lists_word")
@@map("moderation_word_lists")
}
model ContentReport {
id Int @id @default(autoincrement())
mediaId Int @map("media_id")
sessionId String? @map("session_id")
userId String? @map("user_id")
reportType ReportType @map("report_type")
description String?
status ReportStatus @default(pending)
resolvedBy String? @map("resolved_by")
resolvedAt DateTime? @map("resolved_at")
resolutionNotes String? @map("resolution_notes")
createdAt DateTime @default(now()) @map("created_at")
// Relations
media Video @relation(fields: [mediaId], references: [id])
session Session? @relation(fields: [sessionId], references: [id])
user User? @relation("ContentReportUser", fields: [userId], references: [id])
resolver User? @relation("ContentReportResolver", fields: [resolvedBy], references: [id])
@@index([mediaId], map: "idx_content_reports_media")
@@index([status], map: "idx_content_reports_status")
@@index([sessionId], map: "idx_content_reports_session")
@@index([createdAt], map: "idx_content_reports_created")
@@map("content_reports")
}
// ============================================================================
// PLAYLISTS
// ============================================================================
model Playlist {
id Int @id @default(autoincrement())
userId String @map("user_id")
name String
description String?
isPublic Boolean? @default(false) @map("is_public")
shareToken String? @unique @map("share_token")
thumbnailMediaId Int? @map("thumbnail_media_id")
videoCount Int? @default(0) @map("video_count")
totalDurationSeconds Int? @default(0) @map("total_duration_seconds")
viewCount Int? @default(0) @map("view_count")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @map("updated_at")
// Relations
user User @relation("UserPlaylists", fields: [userId], references: [id])
videos PlaylistVideo[]
featured FeaturedPlaylist?
views PlaylistView[]
@@unique([userId, name], map: "idx_playlists_user_name")
@@index([userId], map: "idx_playlists_user")
@@index([isPublic], map: "idx_playlists_public")
@@index([shareToken], map: "idx_playlists_share_token")
@@map("playlists")
}
model PlaylistVideo {
id Int @id @default(autoincrement())
playlistId Int @map("playlist_id")
mediaId Int @map("media_id")
position Int @default(0)
addedAt DateTime @default(now()) @map("added_at")
// Relations
playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
media Video @relation(fields: [mediaId], references: [id])
@@index([playlistId], map: "idx_playlist_videos_playlist")
@@index([mediaId], map: "idx_playlist_videos_media")
@@index([playlistId, mediaId], map: "idx_playlist_videos_unique")
@@map("playlist_videos")
}
model FeaturedPlaylist {
id Int @id @default(autoincrement())
playlistId Int @unique @map("playlist_id")
position Int @default(0)
featuredBy String? @map("featured_by")
featuredAt DateTime? @map("featured_at")
// Relations
playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
featurer User? @relation("FeaturedPlaylistFeaturer", fields: [featuredBy], references: [id])
@@index([position], map: "idx_featured_playlists_position")
@@map("featured_playlists")
}
model PlaylistView {
id Int @id @default(autoincrement())
playlistId Int @map("playlist_id")
sessionId String @map("session_id")
createdAt DateTime @default(now()) @map("created_at")
// Relations
playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
session Session @relation(fields: [sessionId], references: [id])
@@index([playlistId], map: "idx_playlist_views_playlist")
@@index([playlistId, sessionId], map: "idx_playlist_views_unique")
@@map("playlist_views")
}
// ============================================================================
// ADVERTISEMENTS
// ============================================================================
model Ad {
id Int @id @default(autoincrement())
type String
variant String?
imagePath String? @map("image_path")
linkUrl String? @map("link_url")
title String?
subtitle String? @db.Text
ctaText String? @map("cta_text")
ctaStyle String? @default("primary") @map("cta_style")
bgColor String? @map("bg_color")
iconEmoji String? @map("icon_emoji")
isSystemAd Boolean @default(false) @map("is_system_ad")
frequency Int @default(6)
visibility String @default("everyone")
isActive Boolean? @default(true) @map("is_active")
position Int? @default(0)
impressionCount Int? @default(0) @map("impression_count")
clickCount Int? @default(0) @map("click_count")
startDate DateTime? @map("start_date")
endDate DateTime? @map("end_date")
productId String? @unique @map("product_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @map("updated_at")
// Relations
impressions AdImpression[]
clicks AdClick[]
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
@@index([type], map: "idx_ads_type")
@@index([isActive], map: "idx_ads_is_active")
@@index([visibility], map: "idx_ads_visibility")
@@map("ads")
}
model AdImpression {
id Int @id @default(autoincrement())
adId Int @map("ad_id")
sessionId String? @map("session_id")
userId String? @map("user_id")
createdAt DateTime @default(now()) @map("created_at")
// Relations
ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade)
session Session? @relation(fields: [sessionId], references: [id])
user User? @relation("AdImpressionUser", fields: [userId], references: [id])
@@index([adId], map: "idx_ad_impressions_ad")
@@index([sessionId], map: "idx_ad_impressions_session")
@@index([createdAt], map: "idx_ad_impressions_date")
@@map("ad_impressions")
}
model AdClick {
id Int @id @default(autoincrement())
adId Int @map("ad_id")
sessionId String? @map("session_id")
userId String? @map("user_id")
createdAt DateTime @default(now()) @map("created_at")
// Relations
ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade)
session Session? @relation(fields: [sessionId], references: [id])
user User? @relation("AdClickUser", fields: [userId], references: [id])
@@index([adId], map: "idx_ad_clicks_ad")
@@index([sessionId], map: "idx_ad_clicks_session")
@@index([createdAt], map: "idx_ad_clicks_date")
@@map("ad_clicks")
}
// ============================================================================
// FRIENDS & SOCIAL
// ============================================================================
model Friendship {
id Int @id @default(autoincrement())
userId String @map("user_id")
friendId String @map("friend_id")
status FriendshipStatus @default(pending)
createdAt DateTime @default(now()) @map("created_at")
acceptedAt DateTime? @map("accepted_at")
// Relations
user User @relation("UserFriendships", fields: [userId], references: [id])
friend User @relation("UserFriends", fields: [friendId], references: [id])
@@index([userId, friendId], map: "idx_friendships_user_friend")
@@index([userId], map: "idx_friendships_user")
@@index([friendId], map: "idx_friendships_friend")
@@index([status], map: "idx_friendships_status")
@@map("friendships")
}
model UserBlock {
id Int @id @default(autoincrement())
userId String @map("user_id")
blockedUserId String @map("blocked_user_id")
createdAt DateTime @default(now()) @map("created_at")
// Relations
user User @relation("UserBlocks", fields: [userId], references: [id])
blockedUser User @relation("UserBlockedBy", fields: [blockedUserId], references: [id])
@@index([userId, blockedUserId], map: "idx_user_blocks_unique")
@@index([userId], map: "idx_user_blocks_user")
@@map("user_blocks")
}
model Poke {
id Int @id @default(autoincrement())
fromUserId String @map("from_user_id")
toUserId String @map("to_user_id")
isRead Boolean? @default(false) @map("is_read")
createdAt DateTime @default(now()) @map("created_at")
// Relations
from User @relation("PokesSent", fields: [fromUserId], references: [id])
to User @relation("PokesReceived", fields: [toUserId], references: [id])
@@index([toUserId], map: "idx_pokes_to_user")
@@index([fromUserId], map: "idx_pokes_from_user")
@@map("pokes")
}
model VideoRecommendation {
id Int @id @default(autoincrement())
fromUserId String @map("from_user_id")
toUserId String @map("to_user_id")
mediaId Int @map("media_id")
message String?
isRead Boolean? @default(false) @map("is_read")
createdAt DateTime @default(now()) @map("created_at")
// Relations
from User @relation("RecommendationsSent", fields: [fromUserId], references: [id])
to User @relation("RecommendationsReceived", fields: [toUserId], references: [id])
media Video @relation(fields: [mediaId], references: [id])
@@index([toUserId], map: "idx_video_recommendations_to_user")
@@index([fromUserId], map: "idx_video_recommendations_from_user")
@@index([mediaId], map: "idx_video_recommendations_media")
@@map("video_recommendations")
}
model UserPresence {
id Int @id @default(autoincrement())
userId String @unique @map("user_id")
isOnline Boolean? @default(false) @map("is_online")
currentMediaId Int? @map("current_media_id")
lastActivityAt DateTime? @map("last_activity_at")
lastVideoChangeAt DateTime? @map("last_video_change_at")
// Relations
user User @relation("UserPresence", fields: [userId], references: [id])
@@index([isOnline], map: "idx_user_presence_online")
@@index([userId], map: "idx_user_presence_user")
@@map("user_presence")
}
model UserGalleryImage {
id Int @id @default(autoincrement())
userId String @map("user_id")
filename String
originalFilename String? @map("original_filename")
position Int @default(0)
uploadedAt DateTime @default(now()) @map("uploaded_at")
// Relations
user User @relation("UserGalleryImages", fields: [userId], references: [id])
@@index([userId], map: "idx_user_gallery_user")
@@index([userId, position], map: "idx_user_gallery_position")
@@map("user_gallery_images")
}
model UserSocialLink {
id Int @id @default(autoincrement())
userId String @map("user_id")
platform SocialPlatform
url String
displayName String? @map("display_name")
position Int @default(0)
createdAt DateTime @default(now()) @map("created_at")
// Relations
user User @relation("UserSocialLinks", fields: [userId], references: [id])
@@index([userId], map: "idx_user_social_links_user")
@@index([userId, position], map: "idx_user_social_links_position")
@@map("user_social_links")
}
model PrivacySettings {
id Int @id @default(autoincrement())
userId String @unique @map("user_id")
showOnlineStatus Boolean? @default(true) @map("show_online_status")
showCurrentlyWatching Boolean? @default(true) @map("show_currently_watching")
showInFriendActivity Boolean? @default(true) @map("show_in_friend_activity")
anonymizePublicComments Boolean? @default(false) @map("anonymize_public_comments")
hidePublicReactions Boolean? @default(false) @map("hide_public_reactions")
hidePublicFinishes Boolean? @default(false) @map("hide_public_finishes")
allowFriendRequests Boolean? @default(true) @map("allow_friend_requests")
closeFriendsOnlyWatching Boolean? @default(false) @map("close_friends_only_watching")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @map("updated_at")
// Relations
user User @relation("PrivacySettings", fields: [userId], references: [id], onDelete: Cascade)
@@index([userId], map: "idx_privacy_settings_user")
@@map("privacy_settings")
}
model CloseFriend {
id Int @id @default(autoincrement())
userId String @map("user_id")
closeFriendId String @map("close_friend_id")
addedAt DateTime @default(now()) @map("added_at")
// Relations
user User @relation("CloseFriends", fields: [userId], references: [id], onDelete: Cascade)
closeFriend User @relation("CloseFriendOf", fields: [closeFriendId], references: [id], onDelete: Cascade)
@@unique([userId, closeFriendId], map: "idx_close_friends_unique")
@@index([userId], map: "idx_close_friends_user")
@@map("close_friends")
}
// ============================================================================
// USER UPLOADS
// ============================================================================
model UserUpload {
id Int @id @default(autoincrement())
userId String @map("user_id")
filename String
originalFilename String? @map("original_filename")
path String
durationSeconds Int? @map("duration_seconds")
quality String?
orientation String?
fileSize BigInt? @map("file_size")
thumbnailPath String? @map("thumbnail_path")
status UserUploadStatus @default(pending)
reviewedBy String? @map("reviewed_by")
reviewedAt DateTime? @map("reviewed_at")
reviewNotes String? @map("review_notes")
publicMediaId Int? @map("public_media_id")
uploadInviteId Int? @map("upload_invite_id")
createdAt DateTime @default(now()) @map("created_at")
// Relations
user User @relation("UserUploads", fields: [userId], references: [id])
reviewer User? @relation("UserUploadReviewer", fields: [reviewedBy], references: [id])
uploadInvite UploadInvite? @relation(fields: [uploadInviteId], references: [id])
suggestedTags UserUploadSuggestedTag[]
@@index([userId], map: "idx_user_uploads_user")
@@index([status], map: "idx_user_uploads_status")
@@index([createdAt], map: "idx_user_uploads_created")
@@index([uploadInviteId], map: "idx_user_uploads_invite")
@@map("user_uploads")
}
model UploadInvite {
id Int @id @default(autoincrement())
code String @unique
label String?
createdBy String @map("created_by")
status UploadInviteStatus @default(active)
maxUploads Int? @map("max_uploads")
uploadCount Int @default(0) @map("upload_count")
expiresAt DateTime? @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
// Relations
creator User @relation("UploadInviteCreator", fields: [createdBy], references: [id])
uploads UserUpload[]
@@index([code], map: "idx_upload_invites_code")
@@index([status], map: "idx_upload_invites_status")
@@index([createdBy], map: "idx_upload_invites_created_by")
@@map("upload_invites")
}
// ============================================================================
// TAG SYSTEM
// ============================================================================
model TagCategory {
id Int @id @default(autoincrement())
name String @unique
displayOrder Int @default(0) @map("display_order")
createdAt DateTime @default(now()) @map("created_at")
// Relations
tags Tag[]
@@index([displayOrder], map: "idx_tag_categories_display_order")
@@map("tag_categories")
}
model Tag {
id Int @id @default(autoincrement())
categoryId Int @map("category_id")
name String
displayOrder Int @default(0) @map("display_order")
createdAt DateTime @default(now()) @map("created_at")
// Relations
category TagCategory @relation(fields: [categoryId], references: [id])
publicMedia PublicMediaTag[]
userUploadSuggestions UserUploadSuggestedTag[]
userPreferences UserTagPreference[]
@@index([categoryId], map: "idx_tags_category")
@@index([displayOrder], map: "idx_tags_display_order")
@@index([categoryId, name], map: "idx_tags_unique_name")
@@map("tags")
}
model PublicMediaTag {
id Int @id @default(autoincrement())
mediaId Int @map("media_id")
tagId Int @map("tag_id")
addedAt DateTime @default(now()) @map("added_at")
// Relations
media Video @relation("PublicMediaTags", fields: [mediaId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
@@index([mediaId], map: "idx_public_media_tags_media")
@@index([tagId], map: "idx_public_media_tags_tag")
@@index([mediaId, tagId], map: "idx_public_media_tags_unique")
@@map("public_media_tags")
}
model UserUploadSuggestedTag {
id Int @id @default(autoincrement())
uploadId Int @map("upload_id")
tagId Int @map("tag_id")
suggestedAt DateTime @default(now()) @map("suggested_at")
// Relations
upload UserUpload @relation(fields: [uploadId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
@@index([uploadId], map: "idx_user_upload_suggested_tags_upload")
@@index([tagId], map: "idx_user_upload_suggested_tags_tag")
@@index([uploadId, tagId], map: "idx_user_upload_suggested_tags_unique")
@@map("user_upload_suggested_tags")
}
model UserTagPreference {
id Int @id @default(autoincrement())
userId String @map("user_id")
tagId Int @map("tag_id")
savedAt DateTime @default(now()) @map("saved_at")
// Relations
user User @relation("UserTagPreferences", fields: [userId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
@@index([userId], map: "idx_user_tag_preferences_user")
@@index([tagId], map: "idx_user_tag_preferences_tag")
@@index([userId, tagId], map: "idx_user_tag_preferences_unique")
@@map("user_tag_preferences")
}
model PublicMediaPerformer {
id Int @id @default(autoincrement())
mediaId Int @map("media_id")
performerId Int @map("performer_id")
addedAt DateTime @default(now()) @map("added_at")
// Relations
media Video @relation("PublicMediaPerformers", fields: [mediaId], references: [id])
performer Creator @relation(fields: [performerId], references: [id])
@@unique([mediaId, performerId], map: "idx_public_media_performers_unique")
@@index([mediaId], map: "idx_public_media_performers_media")
@@index([performerId], map: "idx_public_media_performers_performer")
@@map("public_media_performers")
}
// ============================================================================
// VIDEO DIGEST SYSTEM
// ============================================================================
model VideoDigest {
id Int @id @default(autoincrement())
videoId Int @map("video_id")
jobId Int? @unique @map("job_id")
status DigestStatus @default(pending)
progress Int? @default(0)
frameCount Int? @map("frame_count")
config Json?
frameAnalyses Json? @map("frame_analyses")
transcript Json?
tags Json?
suggestedClips Json? @map("suggested_clips")
adCutSpec Json? @map("ad_cut_spec")
stageResults Json? @map("stage_results")
createdAt DateTime @default(now()) @map("created_at")
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
error String?
// Relations
video Video @relation(fields: [videoId], references: [id])
job Job? @relation(fields: [jobId], references: [id])
digestVideoTags DigestVideoTag[]
selectedClips DigestSelectedClip[]
generatedClips DigestGeneratedClip[]
outputFolders DigestOutputFolder[]
compilations DigestCompilation[]
generatedScenes DigestGeneratedScene[]
suggestedTags DigestSuggestedTag[]
@@index([videoId], map: "idx_video_digests_video")
@@index([status], map: "idx_video_digests_status")
@@index([createdAt], map: "idx_video_digests_created")
@@map("video_digests")
}
model DigestVideoTag {
id Int @id @default(autoincrement())
digestId Int @map("digest_id")
videoId Int @map("video_id")
category String
value String
confidence Int?
source String? @default("digest")
evidence Json?
createdAt DateTime @default(now()) @map("created_at")
// Relations
digest VideoDigest @relation(fields: [digestId], references: [id])
video Video @relation(fields: [videoId], references: [id])
@@index([digestId], map: "idx_digest_video_tags_digest")
@@index([videoId], map: "idx_digest_video_tags_video")
@@index([category], map: "idx_digest_video_tags_category")
@@index([value], map: "idx_digest_video_tags_value")
@@index([category, value], map: "idx_digest_video_tags_cat_val")
@@map("digest_video_tags")
}
model DigestSelectedClip {
id Int @id @default(autoincrement())
digestId Int @map("digest_id")
videoId Int @map("video_id")
clipType ClipType @map("clip_type")
startTime Int @map("start_time")
endTime Int @map("end_time")
duration Int
reason String?
interestScore Int? @map("interest_score")
position String?
transcriptHint String? @map("transcript_hint")
tags Json?
source ClipSource @default(machine)
isIncluded Int @default(1) @map("is_included")
isHook Int @default(0) @map("is_hook")
sequenceOrder Int @default(0) @map("sequence_order")
hookSourceClipId Int? @map("hook_source_clip_id")
createdAt DateTime @default(now()) @map("created_at")
// Relations
digest VideoDigest @relation(fields: [digestId], references: [id])
video Video @relation(fields: [videoId], references: [id])
generatedClips DigestGeneratedClip[]
@@index([digestId], map: "idx_digest_selected_clips_digest")
@@index([videoId], map: "idx_digest_selected_clips_video")
@@index([clipType], map: "idx_digest_selected_clips_type")
@@index([source], map: "idx_digest_selected_clips_source")
@@index([sequenceOrder], map: "idx_digest_selected_clips_sequence")
@@index([isHook], map: "idx_digest_selected_clips_is_hook")
@@index([hookSourceClipId], map: "idx_digest_selected_clips_hook_source")
@@map("digest_selected_clips")
}
model DigestGeneratedClip {
id Int @id @default(autoincrement())
selectedClipId Int @map("selected_clip_id")
digestId Int @map("digest_id")
folderId Int? @map("folder_id")
clipPath String? @map("clip_path")
gifPath String? @map("gif_path")
status ClipStatus @default(pending)
error String?
createdAt DateTime @default(now()) @map("created_at")
completedAt DateTime? @map("completed_at")
publishedToPublicMediaId Int? @map("published_to_public_media_id")
publishedAt DateTime? @map("published_at")
// Relations
selectedClip DigestSelectedClip @relation(fields: [selectedClipId], references: [id])
digest VideoDigest @relation(fields: [digestId], references: [id])
folder DigestOutputFolder? @relation(fields: [folderId], references: [id])
@@index([selectedClipId], map: "idx_digest_generated_clips_selected")
@@index([digestId], map: "idx_digest_generated_clips_digest")
@@index([status], map: "idx_digest_generated_clips_status")
@@index([folderId], map: "idx_digest_generated_clips_folder")
@@map("digest_generated_clips")
}
model DigestCompilation {
id Int @id @default(autoincrement())
digestId Int @map("digest_id")
videoId Int @map("video_id")
folderId Int? @map("folder_id")
filename String
name String?
path String
durationSeconds Int? @map("duration_seconds")
orientation String?
status String @default("pending")
error String?
hasCaptions Int @default(0) @map("has_captions")
captionStyle Json? @map("caption_style")
closingAdPath String? @map("closing_ad_path")
closingAdDuration Int? @map("closing_ad_duration")
tags Json?
createdAt DateTime @default(now()) @map("created_at")
completedAt DateTime? @map("completed_at")
// Relations
digest VideoDigest @relation(fields: [digestId], references: [id])
video Video @relation(fields: [videoId], references: [id])
folder DigestOutputFolder? @relation(fields: [folderId], references: [id])
@@index([digestId], map: "idx_digest_compilations_digest")
@@index([videoId], map: "idx_digest_compilations_video")
@@index([status], map: "idx_digest_compilations_status")
@@index([folderId], map: "idx_digest_compilations_folder")
@@map("digest_compilations")
}
model DigestOutputFolder {
id Int @id @default(autoincrement())
digestId Int @map("digest_id")
videoId Int @map("video_id")
folderPath String @map("folder_path")
folderName String @map("folder_name")
folderType String @default("clips") @map("folder_type")
clipCount Int? @default(0) @map("clip_count")
compilationCount Int? @default(0) @map("compilation_count")
totalSize Int? @map("total_size")
createdAt DateTime @default(now()) @map("created_at")
// Relations
digest VideoDigest @relation(fields: [digestId], references: [id])
video Video @relation(fields: [videoId], references: [id])
generatedClips DigestGeneratedClip[]
generatedScenes DigestGeneratedScene[]
compilations DigestCompilation[]
@@index([digestId], map: "idx_digest_output_folders_digest")
@@index([videoId], map: "idx_digest_output_folders_video")
@@index([folderPath], map: "idx_digest_output_folders_path")
@@map("digest_output_folders")
}
model DigestGeneratedScene {
id Int @id @default(autoincrement())
digestId Int @map("digest_id")
videoId Int @map("video_id")
folderId Int? @map("folder_id")
sceneNumber Int @map("scene_number")
scenePath String? @map("scene_path")
startTime Float @map("start_time") @db.Real
endTime Float @map("end_time") @db.Real
duration Float @db.Real
tags Json?
dominantPosition String? @map("dominant_position")
createdAt DateTime @default(now()) @map("created_at")
publishedToPublicMediaId Int? @map("published_to_public_media_id")
publishedAt DateTime? @map("published_at")
// Relations
digest VideoDigest @relation(fields: [digestId], references: [id])
video Video @relation(fields: [videoId], references: [id])
folder DigestOutputFolder? @relation(fields: [folderId], references: [id])
@@index([digestId], map: "idx_digest_generated_scenes_digest")
@@index([videoId], map: "idx_digest_generated_scenes_video")
@@index([folderId], map: "idx_digest_generated_scenes_folder")
@@index([scenePath], map: "idx_digest_generated_scenes_path")
@@map("digest_generated_scenes")
}
model VideoSceneCut {
id Int @id @default(autoincrement())
videoId Int @map("video_id")
cuts Json
sceneCount Int @map("scene_count")
duration Float @db.Real
detector String @default("content")
threshold Float @default(27.0) @db.Real
transnetCuts Json? @map("transnet_cuts")
pyscenedetectCuts Json? @map("pyscenedetect_cuts")
clipCuts Json? @map("clip_cuts")
mergedCuts Json? @map("merged_cuts")
analysisMetadata Json? @map("analysis_metadata")
createdAt DateTime @default(now()) @map("created_at")
// Relations
video Video @relation(fields: [videoId], references: [id])
@@index([videoId], map: "idx_video_scene_cuts_video")
@@index([detector], map: "idx_video_scene_cuts_detector")
@@map("video_scene_cuts")
}
model VideoTagTimeline {
id Int @id @default(autoincrement())
videoId Int @map("video_id")
category String
value String
startTime Float @map("start_time") @db.Real
endTime Float @map("end_time") @db.Real
confidence Int?
source String? @default("digest")
createdAt DateTime @default(now()) @map("created_at")
// Relations
video Video @relation(fields: [videoId], references: [id])
@@index([videoId], map: "idx_video_tag_timeline_video")
@@index([category], map: "idx_video_tag_timeline_category")
@@index([value], map: "idx_video_tag_timeline_value")
@@index([category, value], map: "idx_video_tag_timeline_cat_val")
@@index([videoId, startTime], map: "idx_video_tag_timeline_time")
@@map("video_tag_timeline")
}
model VideoSegment {
id Int @id @default(autoincrement())
videoId Int @map("video_id")
segmentType SegmentType @map("segment_type")
startTime Float @map("start_time") @db.Real
endTime Float @map("end_time") @db.Real
duration Float @db.Real
tags Json?
vocalCategory VocalCategory? @map("vocal_category")
transcript String?
dominantPosition String? @map("dominant_position")
interestScore Int? @map("interest_score")
createdAt DateTime @default(now()) @map("created_at")
// Relations
video Video @relation(fields: [videoId], references: [id])
@@index([videoId], map: "idx_video_segments_video")
@@index([segmentType], map: "idx_video_segments_type")
@@index([vocalCategory], map: "idx_video_segments_vocal_category")
@@index([videoId, startTime], map: "idx_video_segments_time")
@@map("video_segments")
}
model VideoTag {
id Int @id @default(autoincrement())
videoId Int @map("video_id")
category String
value String
confidence Int?
source String? @default("digest")
evidence Json?
createdAt DateTime @default(now()) @map("created_at")
// Relations
video Video @relation(fields: [videoId], references: [id])
@@index([videoId], map: "idx_video_tags_video")
@@index([category], map: "idx_video_tags_category")
@@index([value], map: "idx_video_tags_value")
@@index([category, value], map: "idx_video_tags_cat_val")
@@map("video_tags")
}
model DigestSuggestedTag {
id Int @id @default(autoincrement())
digestId Int @map("digest_id")
category String
value String
confidence Int?
evidence Json?
status SuggestedTagStatus @default(pending)
mappedTagId Int? @map("mapped_tag_id")
createdAt DateTime @default(now()) @map("created_at")
reviewedAt DateTime? @map("reviewed_at")
// Relations
digest VideoDigest @relation(fields: [digestId], references: [id])
@@index([digestId], map: "idx_digest_suggested_tags_digest")
@@index([status], map: "idx_digest_suggested_tags_status")
@@index([category], map: "idx_digest_suggested_tags_category")
@@index([mappedTagId], map: "idx_digest_suggested_tags_mapped")
@@map("digest_suggested_tags")
}
model DigestClipTag {
id Int @id @default(autoincrement())
digestId Int @map("digest_id")
clipId Int @map("clip_id")
videoId Int @map("video_id")
category String
value String
confidence Int?
source String? @default("clip_analysis")
evidence Json?
createdAt DateTime @default(now()) @map("created_at")
// Relations
video Video @relation(fields: [videoId], references: [id])
@@index([digestId], map: "idx_digest_clip_tags_digest")
@@index([clipId], map: "idx_digest_clip_tags_clip")
@@index([videoId], map: "idx_digest_clip_tags_video")
@@index([category], map: "idx_digest_clip_tags_category")
@@index([value], map: "idx_digest_clip_tags_value")
@@map("digest_clip_tags")
}
// ============================================================================
// WATCH PARTY SYSTEM
// ============================================================================
model WatchPartySession {
id Int @id @default(autoincrement())
hostUserId String @map("host_user_id")
mediaId Int @map("media_id")
status WatchPartyStatus @default(active)
inviteCode String @unique @map("invite_code")
currentTime Int @default(0) @map("current_time")
isPlaying Boolean @default(false) @map("is_playing")
createdAt DateTime @default(now()) @map("created_at")
endedAt DateTime? @map("ended_at")
// Relations
host User @relation("WatchPartyHost", fields: [hostUserId], references: [id])
participants WatchPartyParticipant[]
messages WatchPartyChatMessage[]
reactions WatchPartyReaction[]
invites WatchPartyInvite[]
@@index([hostUserId], map: "idx_watch_party_sessions_host")
@@index([mediaId], map: "idx_watch_party_sessions_media")
@@index([status], map: "idx_watch_party_sessions_status")
@@index([inviteCode], map: "idx_watch_party_sessions_invite")
@@map("watch_party_sessions")
}
model WatchPartyParticipant {
id Int @id @default(autoincrement())
sessionId Int @map("session_id")
userId String @map("user_id")
joinedAt DateTime @default(now()) @map("joined_at")
leftAt DateTime? @map("left_at")
// Relations
session WatchPartySession @relation(fields: [sessionId], references: [id])
user User @relation("WatchPartyParticipant", fields: [userId], references: [id])
@@index([sessionId], map: "idx_watch_party_participants_session")
@@index([userId], map: "idx_watch_party_participants_user")
@@map("watch_party_participants")
}
model WatchPartyChatMessage {
id Int @id @default(autoincrement())
sessionId Int @map("session_id")
userId String @map("user_id")
commentId Int @map("comment_id")
createdAt DateTime @default(now()) @map("created_at")
// Relations
session WatchPartySession @relation(fields: [sessionId], references: [id])
user User @relation("WatchPartyChatUser", fields: [userId], references: [id])
comment Comment @relation(fields: [commentId], references: [id])
@@index([sessionId], map: "idx_watch_party_chat_session")
@@index([userId], map: "idx_watch_party_chat_user")
@@map("watch_party_chat_messages")
}
model WatchPartyReaction {
id Int @id @default(autoincrement())
sessionId Int @map("session_id")
userId String @map("user_id")
reactionType ReactionType @map("reaction_type")
videoTimestamp Int @map("video_timestamp")
createdAt DateTime @default(now()) @map("created_at")
// Relations
session WatchPartySession @relation(fields: [sessionId], references: [id])
user User @relation("WatchPartyReactionUser", fields: [userId], references: [id])
@@index([sessionId], map: "idx_watch_party_reactions_session")
@@index([userId], map: "idx_watch_party_reactions_user")
@@map("watch_party_reactions")
}
model WatchPartyInvite {
id Int @id @default(autoincrement())
sessionId Int @map("session_id")
userId String @map("user_id")
invitedBy String @map("invited_by")
status String @default("pending")
createdAt DateTime @default(now()) @map("created_at")
acceptedAt DateTime? @map("accepted_at")
// Relations
session WatchPartySession @relation(fields: [sessionId], references: [id])
user User @relation("WatchPartyInvitee", fields: [userId], references: [id])
inviter User @relation("WatchPartyInviter", fields: [invitedBy], references: [id])
@@index([sessionId], map: "idx_watch_party_invites_session")
@@index([userId], map: "idx_watch_party_invites_user")
@@map("watch_party_invites")
}
// ============================================================================
// TAG GENERATION JOBS
// ============================================================================
model TagGenerationJob {
id Int @id @default(autoincrement())
videoId Int @map("video_id")
jobId Int? @map("job_id")
status String @default("pending")
progress Int? @default(0)
generatedTags Json? @map("generated_tags")
rawResponse String? @map("raw_response")
error String?
createdAt DateTime @default(now()) @map("created_at")
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
// Relations
video Video @relation(fields: [videoId], references: [id])
job Job? @relation(fields: [jobId], references: [id])
@@index([videoId], map: "idx_tag_generation_jobs_video")
@@index([status], map: "idx_tag_generation_jobs_status")
@@map("tag_generation_jobs")
}
// ============================================================================
// CREATORS & PERFORMERS
// ============================================================================
model Creator {
id Int @id @default(autoincrement())
name String @unique
stage_name String? @map("stage_name")
performerGender String? @map("performer_gender")
faceEmbedding Json? @map("face_embedding")
referenceImagePath String? @map("reference_image_path")
performerCategory String? @map("performer_category")
status String @default("active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @map("updated_at")
// Relations
publicMediaPerformers PublicMediaPerformer[]
performerFaces PerformerFace[]
performerDiscrepancies PerformerDiscrepancy[]
@@index([name], map: "idx_creators_name")
@@index([performerCategory], map: "idx_creators_category")
@@map("creators")
}
model PerformerFace {
id Int @id @default(autoincrement())
performerId Int @map("performer_id")
sourcePath String @map("source_path")
frameTimestamp Float? @map("frame_timestamp") @db.Real
bbox Json
embedding Json
gender Int
age Int
detScore Float @map("det_score") @db.Real
verifiedByUser Boolean @default(false) @map("verified_by_user")
confidence Float? @db.Real
isReferenceImage Boolean @default(false) @map("is_reference_image")
createdAt DateTime @default(now()) @map("created_at")
// Relations
performer Creator @relation(fields: [performerId], references: [id])
@@index([performerId], map: "idx_performer_faces_performer")
@@index([sourcePath], map: "idx_performer_faces_source")
@@index([isReferenceImage], map: "idx_performer_faces_reference")
@@map("performer_faces")
}
model PerformerDiscrepancy {
id Int @id @default(autoincrement())
videoPath String @map("video_path")
frameTimestamp Float @map("frame_timestamp") @db.Real
assignedPerformerId Int @map("assigned_performer_id")
detectedPerformerId Int? @map("detected_performer_id")
similarity Float @map("similarity") @db.Real
faceData Json @map("face_data")
resolutionStatus String @default("pending") @map("resolution_status")
resolutionAction String? @map("resolution_action")
resolutionNotes String? @map("resolution_notes")
resolvedBy String? @map("resolved_by")
resolvedAt DateTime? @map("resolved_at")
createdAt DateTime @default(now()) @map("created_at")
// Relations
assignedPerformer Creator @relation(fields: [assignedPerformerId], references: [id])
resolver User? @relation("PerformerDiscrepancyResolver", fields: [resolvedBy], references: [id])
@@index([assignedPerformerId], map: "idx_performer_discrepancies_assigned")
@@index([detectedPerformerId], map: "idx_performer_discrepancies_detected")
@@index([resolutionStatus], map: "idx_performer_discrepancies_status")
@@map("performer_discrepancies")
}
// ============================================================================
// PIPELINE SYSTEM
// ============================================================================
model Pipeline {
id Int @id @default(autoincrement())
name String
description String?
status PipelineStatus @default(draft)
config Json?
templateId Int? @map("template_id")
createdAt DateTime @default(now()) @map("created_at")
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
error String?
// Relations
template PipelineTemplate? @relation(fields: [templateId], references: [id])
steps PipelineStep[]
jobs Job[]
@@index([status], map: "idx_pipelines_status")
@@index([templateId], map: "idx_pipelines_template")
@@map("pipelines")
}
model PipelineStep {
id Int @id @default(autoincrement())
pipelineId Int @map("pipeline_id")
stepName String @map("step_name")
stepType String @map("step_type")
sequenceOrder Int @map("sequence_order")
status PipelineStepStatus @default(pending)
config Json?
inputs Json?
outputs Json?
dependsOn Json? @map("depends_on")
error String?
createdAt DateTime @default(now()) @map("created_at")
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
// Relations
pipeline Pipeline @relation(fields: [pipelineId], references: [id])
jobs Job[]
events PipelineStepEvent[]
@@index([pipelineId], map: "idx_pipeline_steps_pipeline")
@@index([status], map: "idx_pipeline_steps_status")
@@index([sequenceOrder], map: "idx_pipeline_steps_sequence")
@@map("pipeline_steps")
}
model PipelineStepEvent {
id Int @id @default(autoincrement())
stepId Int @map("step_id")
eventType String @map("event_type")
message String?
data Json?
createdAt DateTime @default(now()) @map("created_at")
// Relations
step PipelineStep @relation(fields: [stepId], references: [id])
@@index([stepId], map: "idx_pipeline_step_events_step")
@@index([eventType], map: "idx_pipeline_step_events_type")
@@map("pipeline_step_events")
}
model ResourceSnapshot {
id Int @id @default(autoincrement())
timestamp DateTime @default(now())
cpuPercent Float? @map("cpu_percent") @db.Real
memoryMb Int? @map("memory_mb")
gpuPercent Float? @map("gpu_percent") @db.Real
vramMb Int? @map("vram_mb")
diskUsageMb Int? @map("disk_usage_mb")
activeJobs Int? @map("active_jobs")
queuedJobs Int? @map("queued_jobs")
metadata Json?
@@index([timestamp], map: "idx_resource_snapshots_timestamp")
@@map("resource_snapshots")
}
model PipelineTemplate {
id Int @id @default(autoincrement())
name String
description String?
category String?
defaultConfig Json? @map("default_config")
stepDefinitions Json @map("step_definitions")
isPublic Boolean @default(false) @map("is_public")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @map("updated_at")
// Relations
pipelines Pipeline[]
@@index([category], map: "idx_pipeline_templates_category")
@@index([isPublic], map: "idx_pipeline_templates_public")
@@map("pipeline_templates")
}
// ============================================================================
// SUBSCRIPTION & PAYMENTS
// ============================================================================
model SubscriptionPlan {
id Int @id @default(autoincrement())
name String
priceCAD Int @map("price_cad")
durationDays Int @map("duration_days")
features Json?
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
// Stripe integration
stripeProductId String? @map("stripe_product_id")
stripePriceId String? @map("stripe_price_id")
stripeYearlyPriceId String? @map("stripe_yearly_price_id")
yearlyPriceCAD Int? @map("yearly_price_cad")
description String? @db.Text
tier Int @default(0)
displayOrder Int @default(0) @map("display_order")
// Relations
subscriptions UserSubscription[]
@@map("subscription_plans")
}
model UserSubscription {
id Int @id @default(autoincrement())
userId String @map("user_id")
planId Int @map("plan_id")
status SubscriptionStatus @default(active)
startDate DateTime @map("start_date")
endDate DateTime @map("end_date")
cancelledAt DateTime? @map("cancelled_at")
createdAt DateTime @default(now()) @map("created_at")
// Stripe integration
stripeSubscriptionId String? @unique @map("stripe_subscription_id")
stripeCustomerId String? @map("stripe_customer_id")
currentPeriodEnd DateTime? @map("current_period_end")
cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end")
// Relations
user User @relation("UserSubscriptions", fields: [userId], references: [id])
plan SubscriptionPlan @relation(fields: [planId], references: [id])
@@index([userId], map: "idx_user_subscriptions_user")
@@index([planId], map: "idx_user_subscriptions_plan")
@@index([status], map: "idx_user_subscriptions_status")
@@map("user_subscriptions")
}
model Invoice {
id Int @id @default(autoincrement())
userId String @map("user_id")
subscriptionId Int? @map("subscription_id")
amountCAD Int @map("amount_cad")
status InvoiceStatus @default(pending)
issuedAt DateTime @default(now()) @map("issued_at")
paidAt DateTime? @map("paid_at")
dueDate DateTime @map("due_date")
description String?
metadata Json?
// Stripe integration
stripeInvoiceId String? @unique @map("stripe_invoice_id")
type String @default("subscription") @map("invoice_type") // subscription|product|donation
// Relations
user User @relation("UserInvoices", fields: [userId], references: [id])
payments Payment[]
@@index([userId], map: "idx_invoices_user")
@@index([status], map: "idx_invoices_status")
@@index([issuedAt], map: "idx_invoices_issued")
@@map("invoices")
}
model Payment {
id Int @id @default(autoincrement())
invoiceId Int @map("invoice_id")
userId String @map("user_id")
amountCAD Int @map("amount_cad")
method PaymentMethod
status PaymentStatus @default(pending)
externalId String? @map("external_id")
metadata Json?
processedAt DateTime? @map("processed_at")
createdAt DateTime @default(now()) @map("created_at")
// Stripe integration
stripePaymentIntentId String? @unique @map("stripe_payment_intent_id")
stripeCheckoutSessionId String? @map("stripe_checkout_session_id")
// Relations
invoice Invoice @relation(fields: [invoiceId], references: [id])
user User @relation("UserPayments", fields: [userId], references: [id])
auditLogs PaymentAuditLog[]
@@index([invoiceId], map: "idx_payments_invoice")
@@index([userId], map: "idx_payments_user")
@@index([status], map: "idx_payments_status")
@@map("payments")
}
model PaymentAuditLog {
id Int @id @default(autoincrement())
paymentId Int @map("payment_id")
action String
oldStatus String? @map("old_status")
newStatus String? @map("new_status")
userId String? @map("user_id")
metadata Json?
createdAt DateTime @default(now()) @map("created_at")
// Relations
payment Payment @relation(fields: [paymentId], references: [id])
user User? @relation("PaymentAuditUser", fields: [userId], references: [id])
@@index([paymentId], map: "idx_payment_audit_log_payment")
@@index([action], map: "idx_payment_audit_log_action")
@@index([createdAt], map: "idx_payment_audit_log_created")
@@map("payment_audit_log")
}
model Product {
id String @id @default(cuid())
slug String @unique
title String
description String? @db.Text
priceCAD Int @map("price_cad")
type ProductType
stripeProductId String? @map("stripe_product_id")
stripePriceId String? @map("stripe_price_id")
isActive Boolean @default(true) @map("is_active")
imageUrl String? @map("image_url")
downloadUrl String? @map("download_url")
metadata Json?
maxPurchases Int? @map("max_purchases")
purchaseCount Int @default(0) @map("purchase_count")
createdByUserId String? @map("created_by_user_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
orders Order[]
galleryAd Ad?
@@index([type], map: "idx_products_type")
@@index([isActive], map: "idx_products_active")
@@map("products")
}
model Order {
id String @id @default(cuid())
userId String? @map("user_id")
productId String? @map("product_id")
amountCAD Int @map("amount_cad")
status OrderStatus @default(PENDING)
stripeCheckoutSessionId String? @unique @map("stripe_checkout_session_id")
stripePaymentIntentId String? @map("stripe_payment_intent_id")
type String @default("product") @map("order_type") // product|donation
// Buyer info (for guests)
buyerEmail String @map("buyer_email")
buyerName String? @map("buyer_name")
// Donation-specific
donorMessage String? @db.Text @map("donor_message")
isAnonymous Boolean @default(false) @map("is_anonymous")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
user User? @relation("UserOrders", fields: [userId], references: [id])
product Product? @relation(fields: [productId], references: [id])
@@index([userId], map: "idx_orders_user")
@@index([productId], map: "idx_orders_product")
@@index([status], map: "idx_orders_status")
@@index([type], map: "idx_orders_type")
@@map("orders")
}
model PaymentSettings {
id String @id @default(cuid())
stripeSecretKey String @default("") @map("stripe_secret_key")
stripePublishableKey String @default("") @map("stripe_publishable_key")
stripeWebhookSecret String @default("") @map("stripe_webhook_secret")
defaultCurrency String @default("cad") @map("default_currency")
// Donation settings
enableDonations Boolean @default(true) @map("enable_donations")
donationSuggestedAmounts Json @default("[1000, 2500, 5000, 10000]") @map("donation_suggested_amounts")
donationMinimum Int @default(500) @map("donation_minimum")
donationPageTitle String @default("Support Our Work") @map("donation_page_title")
donationPageDescription String? @db.Text @map("donation_page_description")
thankYouMessage String @default("Thank you for your support!") @db.Text @map("thank_you_message")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("payment_settings")
}
// ============================================================================
// NOTIFICATIONS
// ============================================================================
model Notification {
id Int @id @default(autoincrement())
userId String @map("user_id")
type NotificationType
title String
message String
metadata Json?
isRead Boolean @default(false) @map("is_read")
createdAt DateTime @default(now()) @map("created_at")
readAt DateTime? @map("read_at")
// Relations
user User @relation("UserNotifications", fields: [userId], references: [id])
@@index([userId], map: "idx_notifications_user")
@@index([isRead], map: "idx_notifications_read")
@@index([type], map: "idx_notifications_type")
@@index([createdAt], map: "idx_notifications_created")
@@map("notifications")
}
model NotificationPreferences {
id Int @id @default(autoincrement())
userId String @unique @map("user_id")
enableFriendRequests Boolean @default(true) @map("enable_friend_requests")
enableComments Boolean @default(true) @map("enable_comments")
enableUploadApprovals Boolean @default(true) @map("enable_upload_approvals")
enableAchievements Boolean @default(true) @map("enable_achievements")
enableSystemUpdates Boolean @default(true) @map("enable_system_updates")
emailNotifications Boolean @default(false) @map("email_notifications")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @map("updated_at")
// Relations
user User @relation("NotificationPreferences", fields: [userId], references: [id])
@@index([userId], map: "idx_notification_preferences_user")
@@map("notification_preferences")
}
// ============================================================================
// GEO-BLOCKING
// ============================================================================
model GeoBlockingRule {
id Int @id @default(autoincrement())
countryCode String @map("country_code")
action String // 'block' | 'allow'
reason String?
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @map("updated_at")
@@index([countryCode], map: "idx_geo_blocking_country")
@@index([isActive], map: "idx_geo_blocking_active")
@@map("geo_blocking_rules")
}
// ============================================================================
// PUBLISHED INBOX FILES
// ============================================================================
model PublishedInboxFile {
id Int @id @default(autoincrement())
originalInboxPath String @map("original_inbox_path")
publishedToPublicMediaId Int @map("published_to_public_media_id")
publishedAt DateTime @map("published_at")
movedToPath String? @map("moved_to_path")
metadataSnapshot Json? @map("metadata_snapshot")
@@index([originalInboxPath], map: "idx_published_inbox_files_inbox_path")
@@index([publishedToPublicMediaId], map: "idx_published_inbox_files_public_media")
@@map("published_inbox_files")
}
// ============================================================================
// VIDEO OCR RESULTS
// ============================================================================
model VideoOcrResult {
id Int @id @default(autoincrement())
videoId Int @map("video_id")
fullText String @map("full_text")
structuredData Json? @map("structured_data")
confidence Float? @db.Real
createdAt DateTime @default(now()) @map("created_at")
// Relations
video Video @relation(fields: [videoId], references: [id])
@@index([videoId], map: "idx_video_ocr_results_video")
@@map("video_ocr_results")
}
// Enhanced video analytics models
model VideoView {
id Int @id @default(autoincrement())
videoId Int @map("video_id")
userId String? @map("user_id")
ipAddressHash String? @map("ip_address_hash") @db.VarChar(64) // SHA-256 hash
userAgentHash String? @map("user_agent_hash") @db.VarChar(64) // SHA-256 hash
referer String? @db.Text
watchTimeSeconds Int @default(0) @map("watch_time_seconds")
completed Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
user User? @relation("VideoViews", fields: [userId], references: [id], onDelete: SetNull)
@@index([videoId], map: "idx_video_views_video")
@@index([userId], map: "idx_video_views_user")
@@index([createdAt], map: "idx_video_views_created")
@@index([videoId, createdAt], map: "idx_video_views_video_created")
@@map("video_views")
}
model VideoEvent {
id Int @id @default(autoincrement())
videoId Int @map("video_id")
viewId Int? @map("view_id")
eventType String @map("event_type") @db.VarChar(50) // play, pause, seek, complete
timestamp Decimal @db.Decimal(10, 2) // Video timestamp in seconds
createdAt DateTime @default(now()) @map("created_at")
// Relations
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
@@index([videoId], map: "idx_video_events_video")
@@index([viewId], map: "idx_video_events_view")
@@index([eventType], map: "idx_video_events_type")
@@index([createdAt], map: "idx_video_events_created")
@@map("video_events")
}
model VideoScheduleHistory {
id Int @id @default(autoincrement())
videoId Int @map("video_id")
action String @db.VarChar(20) // 'publish' or 'unpublish'
scheduledFor DateTime @map("scheduled_for")
executedAt DateTime? @map("executed_at")
status String @db.VarChar(20) // 'pending', 'completed', 'failed', 'cancelled'
error String? @db.Text
scheduledByUserId String @map("scheduled_by_user_id")
createdAt DateTime @default(now()) @map("created_at")
// Relations
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
scheduledBy User @relation("VideoScheduleHistory", fields: [scheduledByUserId], references: [id])
@@index([videoId], map: "idx_video_schedule_history_video")
@@index([scheduledFor], map: "idx_video_schedule_history_scheduled")
@@index([status], map: "idx_video_schedule_history_status")
@@index([scheduledByUserId], map: "idx_video_schedule_history_user")
@@map("video_schedule_history")
}
// ============================================================================
// DOCS ANALYTICS
// ============================================================================
model DocsPageView {
id String @id @default(cuid())
path String // e.g. "/docs/getting-started/"
referrer String? @db.Text // document.referrer
sessionHash String? // anonymous session UUID (sessionStorage)
userAgent String? // for device type breakdown
createdAt DateTime @default(now())
@@index([createdAt])
@@index([path, createdAt])
@@map("docs_page_views")
}