generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum SubscriptionTier { BASIC PLUS PREMIUM } enum IdentifierType { EMAIL PHONE SSN } enum WatchListStatus { ACTIVE PAUSED } enum Severity { LOW INFO MEDIUM WARNING HIGH CRITICAL } enum AlertChannel { EMAIL PUSH SMS } enum AlertStatus { PENDING SENT READ } enum ScanJobStatus { PENDING RUNNING COMPLETED FAILED } enum DataSource { HIBP SECURITY_TRAILS CENSYS SHODAN HONEYPOT } enum AnalysisJobStatus { PENDING RUNNING COMPLETED FAILED } enum AnalysisType { SYNTHETIC_DETECTION VOICE_MATCH BATCH } enum DetectionVerdict { NATURAL SYNTHETIC UNCERTAIN } model User { id String @id @default(uuid()) email String @unique name String? subscriptionTier SubscriptionTier @default(BASIC) familyGroupId String? watchListItems WatchListItem[] alerts Alert[] scanJobs ScanJob[] scanSchedules ScanSchedule[] voiceEnrollments VoiceEnrollment[] analysisJobs AnalysisJob[] spamFeedback SpamFeedback[] spamCallAnalyses SpamCallAnalysis[] spamAuditLogs SpamAuditLog[] normalizedAlerts NormalizedAlert[] correlationGroups CorrelationGroup[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([email]) } model WatchListItem { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) identifierType IdentifierType identifierValue String identifierHash String @unique status WatchListStatus @default(ACTIVE) exposures Exposure[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) @@index([identifierHash]) } model Exposure { id String @id @default(uuid()) watchListItemId String watchListItem WatchListItem @relation(fields: [watchListItemId], references: [id], onDelete: Cascade) dataSource DataSource breachName String exposedAt DateTime dataType String[] severity Severity details String? contentHash String @unique alert Alert? createdAt DateTime @default(now()) @@index([watchListItemId]) @@index([contentHash]) @@index([dataSource]) } model Alert { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) exposureId String @unique exposure Exposure @relation(fields: [exposureId], references: [id], onDelete: Cascade) severity Severity channel AlertChannel status AlertStatus @default(PENDING) dedupKey String sentAt DateTime? createdAt DateTime @default(now()) @@index([userId, status]) @@index([dedupKey]) } model ScanJob { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) status ScanJobStatus @default(PENDING) source DataSource? resultCount Int @default(0) errorMessage String? scheduledBy String? webhookEvents WebhookEvent[] completedAt DateTime? createdAt DateTime @default(now()) @@index([userId, status]) @@index([createdAt]) } enum ScheduleStatus { ACTIVE PAUSED } model ScanSchedule { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) intervalMinutes Int // minutes between scans cronExpression String // cron expression for scheduling status ScheduleStatus @default(ACTIVE) lastScanAt DateTime? nextScanAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([userId]) @@index([status]) } enum WebhookEventType { SCAN_TRIGGER BREACH_DETECTED SUBSCRIPTION_CHANGE } model WebhookEvent { id String @id @default(uuid()) eventType WebhookEventType payload String source String? signature String? processed Boolean @default(false) processedAt DateTime? scanJobId String? scanJob ScanJob? @relation(fields: [scanJobId], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) @@index([eventType, processed]) @@index([createdAt]) } model VoiceEnrollment { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) label String embeddingVector Float[] embeddingDim Int @default(192) audioFilePath String? sampleRate Int @default(16000) durationSec Float? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) @@index([embeddingDim]) } model AnalysisJob { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) analysisType AnalysisType audioFilePath String status AnalysisJobStatus @default(PENDING) result AnalysisResult? errorMessage String? completedAt DateTime? createdAt DateTime @default(now()) @@index([userId, status]) @@index([createdAt]) } model AnalysisResult { id String @id @default(uuid()) analysisJobId String @unique analysisJob AnalysisJob @relation(fields: [analysisJobId], references: [id], onDelete: Cascade) syntheticScore Float verdict DetectionVerdict matchedEnrollmentId String? matchedSimilarity Float? confidence Float processingTimeMs Int modelVersion String? metadata String? createdAt DateTime @default(now()) @@index([analysisJobId]) @@index([verdict]) } enum SpamDecision { BLOCK FLAG ALLOW } model SpamFeedback { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) phoneNumber String // AES-256 encrypted PII phoneNumberHash String // SHA-256 hash for anonymized lookup isSpam Boolean label String? metadata String? // Unbounded JSON createdAt DateTime @default(now()) @@index([userId]) @@index([phoneNumberHash]) @@index([createdAt]) } model SpamCallAnalysis { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) phoneNumber String callTimestamp DateTime hiyaReputationScore Float? truecallerSpamScore Float? decision SpamDecision confidence Float ruleMatches String[] // IDs of matched rules auditLogs SpamAuditLog[] createdAt DateTime @default(now()) @@index([userId]) @@index([phoneNumber]) @@index([callTimestamp]) } model SpamRule { id String @id @default(uuid()) name String @unique pattern String // Regex pattern - needs ReDoS validation decision SpamDecision description String? isActive Boolean @default(true) priority Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([isActive]) @@index([priority]) } model SpamAuditLog { id String @id @default(uuid()) analysisId String? analysis SpamCallAnalysis? @relation(fields: [analysisId], references: [id], onDelete: SetNull) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) phoneNumber String decision SpamDecision reason String ruleId String? createdAt DateTime @default(now()) @@index([userId]) @@index([createdAt]) @@index([decision]) } enum AlertSource { DARKWATCH SPAMSHIELD VOICEPRINT CALL_ANALYSIS } enum AlertCategory { BREACH_EXPOSURE SPAM_CALL SPAM_SMS SYNTHETIC_VOICE VOICE_MISMATCH CALL_QUALITY CALL_ANOMALY CALL_EVENT } enum CorrelationStatus { ACTIVE RESOLVED FALSE_POSITIVE } enum EntityType { PHONE_NUMBER EMAIL USER_ID CALL_ID IP_ADDRESS } model NormalizedAlert { id String @id @default(uuid()) source AlertSource category AlertCategory severity Severity userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) title String description String entities Json // [{ type: EntityType, value: string }] sourceAlertId String groupId String? correlationGroup CorrelationGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull) payload Json createdAt DateTime @default(now()) @@index([userId, createdAt]) @@index([groupId]) @@index([sourceAlertId]) @@index([source]) @@index([severity]) } model CorrelationGroup { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade, map: "corr_user_idx") entities Json // [{ type: EntityType, value: string }] highestSeverity Severity status CorrelationStatus @default(ACTIVE) alertCount Int @default(0) alerts NormalizedAlert[] summary String? resolvedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId, status]) @@index([createdAt]) }