// This is your Prisma schema file datasource db { provider = "postgres" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } generator json { provider = "prisma-json-types-generator" } enum CommentStatus { PENDING HUMAN_PENDING APPROVED VERIFIED REJECTED } enum OrderIdStatus { PENDING APPROVED REJECTED WITHDRAWN } model Comment { id Int @id @default(autoincrement()) /// Computed via trigger. Do not update through prisma. upvotes Int @default(0) status CommentStatus @default(PENDING) suspicious Boolean @default(false) requiresAdminReview Boolean @default(false) communityNote String? verificationNote String? internalNote String? privateContext String? orderId String? @db.VarChar(100) orderIdStatus OrderIdStatus? @default(PENDING) kycRequested Boolean @default(false) fundsBlocked Boolean @default(false) content String rating Int? @db.SmallInt ratingActive Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt author User @relation(fields: [authorId], references: [id], onDelete: Cascade) authorId Int service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade) serviceId Int parentId Int? parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade) replies Comment[] @relation("CommentReplies") karmaTransactions KarmaTransaction[] votes CommentVote[] notificationPreferenceswatchedComments NotificationPreferences[] @relation("watchedComments") Notification Notification[] @@unique([serviceId, orderId], name: "unique_orderId_per_service") @@index([status]) @@index([createdAt]) @@index([serviceId]) @@index([authorId]) @@index([upvotes]) @@index([rating]) @@index([ratingActive]) } enum VerificationStatus { COMMUNITY_CONTRIBUTED // COMMUNITY_VERIFIED APPROVED VERIFICATION_SUCCESS VERIFICATION_FAILED } enum ServiceInfoBanner { NONE NO_LONGER_OPERATIONAL } enum ServiceVisibility { PUBLIC UNLISTED HIDDEN ARCHIVED } enum Currency { MONERO BITCOIN LIGHTNING FIAT CASH } enum EventType { WARNING WARNING_SOLVED ALERT ALERT_SOLVED INFO NORMAL UPDATE } enum ServiceUserRole { OWNER ADMIN MODERATOR SUPPORT TEAM_MEMBER } enum AccountStatusChange { ADMIN_TRUE ADMIN_FALSE VERIFIED_TRUE VERIFIED_FALSE MODERATOR_TRUE MODERATOR_FALSE SPAMMER_TRUE SPAMMER_FALSE } enum NotificationType { TEST COMMENT_STATUS_CHANGE REPLY_COMMENT_CREATED COMMUNITY_NOTE_ADDED /// Comment that is not a reply. May include a rating. ROOT_COMMENT_CREATED SUGGESTION_CREATED SUGGESTION_MESSAGE SUGGESTION_STATUS_CHANGE // KARMA_UNLOCK // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code. KARMA_CHANGE /// Marked as spammer, promoted to admin, etc. ACCOUNT_STATUS_CHANGE EVENT_CREATED SERVICE_VERIFICATION_STATUS_CHANGE ACCOUNT_DELETION_WARNING_30_DAYS ACCOUNT_DELETION_WARNING_15_DAYS ACCOUNT_DELETION_WARNING_5_DAYS ACCOUNT_DELETION_WARNING_1_DAY } enum CommentStatusChange { MARKED_AS_SPAM UNMARKED_AS_SPAM MARKED_FOR_ADMIN_REVIEW UNMARKED_FOR_ADMIN_REVIEW STATUS_CHANGED_TO_APPROVED STATUS_CHANGED_TO_VERIFIED STATUS_CHANGED_TO_REJECTED STATUS_CHANGED_TO_PENDING } enum ServiceVerificationStatusChange { STATUS_CHANGED_TO_COMMUNITY_CONTRIBUTED STATUS_CHANGED_TO_APPROVED STATUS_CHANGED_TO_VERIFICATION_SUCCESS STATUS_CHANGED_TO_VERIFICATION_FAILED } enum ServiceSuggestionStatusChange { STATUS_CHANGED_TO_PENDING STATUS_CHANGED_TO_APPROVED STATUS_CHANGED_TO_REJECTED STATUS_CHANGED_TO_WITHDRAWN } enum KarmaTransactionAction { COMMENT_APPROVED COMMENT_VERIFIED COMMENT_SPAM COMMENT_SPAM_REVERTED COMMENT_UPVOTE COMMENT_DOWNVOTE COMMENT_VOTE_REMOVED SUGGESTION_APPROVED MANUAL_ADJUSTMENT } enum AnnouncementType { INFO WARNING ALERT } model Notification { id Int @id @default(autoincrement()) userId Int user User @relation("NotificationOwner", fields: [userId], references: [id], onDelete: Cascade) type NotificationType read Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt aboutComment Comment? @relation(fields: [aboutCommentId], references: [id]) aboutCommentId Int? aboutEvent Event? @relation(fields: [aboutEventId], references: [id]) aboutEventId Int? aboutService Service? @relation(fields: [aboutServiceId], references: [id]) aboutServiceId Int? aboutServiceSuggestion ServiceSuggestion? @relation(fields: [aboutServiceSuggestionId], references: [id]) aboutServiceSuggestionId Int? aboutServiceSuggestionMessage ServiceSuggestionMessage? @relation(fields: [aboutServiceSuggestionMessageId], references: [id]) aboutServiceSuggestionMessageId Int? aboutAccountStatusChange AccountStatusChange? aboutCommentStatusChange CommentStatusChange? aboutServiceVerificationStatusChange ServiceVerificationStatusChange? aboutSuggestionStatusChange ServiceSuggestionStatusChange? aboutKarmaTransaction KarmaTransaction? @relation(fields: [aboutKarmaTransactionId], references: [id]) aboutKarmaTransactionId Int? @@index([userId]) @@index([read]) @@index([createdAt]) @@index([userId, read, createdAt]) @@index([userId, type, aboutCommentId]) @@index([userId, type, aboutServiceSuggestionMessageId], map: "idx_notification_suggestion_message") @@index([userId, type, aboutServiceSuggestionId], map: "idx_notification_suggestion_status") @@index([userId, type, aboutAccountStatusChange], map: "idx_notification_account_status") } model NotificationPreferences { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt userId Int @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) enableOnMyCommentStatusChange Boolean @default(true) enableAutowatchMyComments Boolean @default(true) enableNotifyPendingRepliesOnWatch Boolean @default(false) karmaNotificationThreshold Int @default(10) onEventCreatedForServices Service[] @relation("onEventCreatedForServices") onRootCommentCreatedForServices Service[] @relation("onRootCommentCreatedForServices") onVerificationChangeForServices Service[] @relation("onVerificationChangeForServices") watchedComments Comment[] @relation("watchedComments") onServiceVerificationChangeFilter NotificationPreferenceOnServiceVerificationChangeFilterFilter[] } model NotificationPreferenceOnServiceVerificationChangeFilterFilter { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt verificationStatus VerificationStepStatus notificationPreferences NotificationPreferences @relation(fields: [notificationPreferencesId], references: [id], onDelete: Cascade) notificationPreferencesId Int categories Category[] attributes Attribute[] currencies Currency[] /// 0-10 scores Int[] @@unique([verificationStatus, notificationPreferencesId]) } model Event { id Int @id @default(autoincrement()) title String content String source String? type EventType visible Boolean @default(true) startedAt DateTime /// If null, the event is ongoing. If same as startedAt, the event is a one-time event. If startedAt is in the future, the event is upcoming. endedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade) serviceId Int Notification Notification[] @@index([visible]) @@index([startedAt]) @@index([createdAt]) @@index([endedAt]) @@index([type]) @@index([serviceId]) } enum ServiceSuggestionStatus { PENDING APPROVED REJECTED WITHDRAWN } enum ServiceSuggestionType { CREATE_SERVICE EDIT_SERVICE } enum KycLevelClarification { NONE DEPENDS_ON_PARTNERS } model ServiceSuggestion { id Int @id @default(autoincrement()) type ServiceSuggestionType status ServiceSuggestionStatus @default(PENDING) notes String? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt userId Int serviceId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade) messages ServiceSuggestionMessage[] Notification Notification[] KarmaTransaction KarmaTransaction[] @@index([userId]) @@index([serviceId]) } model ServiceSuggestionMessage { id Int @id @default(autoincrement()) content String createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt userId Int suggestionId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) suggestion ServiceSuggestion @relation(fields: [suggestionId], references: [id], onDelete: Cascade) notifications Notification[] @@index([userId]) @@index([suggestionId]) @@index([createdAt]) } model Service { id Int @id @default(autoincrement()) name String slug String @unique previousSlugs String[] @default([]) description String categories Category[] @relation("ServiceToCategory") kycLevel Int @default(4) kycLevelClarification KycLevelClarification @default(NONE) /// Date only, no time. operatingSince DateTime? @db.Date overallScore Int @default(0) privacyScore Int @default(0) trustScore Int @default(0) /// Computed via trigger. Do not update through prisma. averageUserRating Float? serviceVisibility ServiceVisibility @default(PUBLIC) serviceInfoBanner ServiceInfoBanner @default(NONE) serviceInfoBannerNotes String? verificationStatus VerificationStatus @default(COMMUNITY_CONTRIBUTED) verificationSummary String? verificationRequests ServiceVerificationRequest[] verificationProofMd String? /// [UserSentiment] userSentiment Json? userSentimentAt DateTime? referral String? acceptedCurrencies Currency[] @default([]) serviceUrls String[] tosUrls String[] @default([]) onionUrls String[] @default([]) i2pUrls String[] @default([]) imageUrl String? /// [TosReview] tosReview Json? tosReviewAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt /// Computed via trigger when the visibility is PUBLIC or (ARCHIVED and listedAt was null). Do not update through prisma. listedAt DateTime? /// Computed via trigger when the verification status is APPROVED. Do not update through prisma. approvedAt DateTime? /// Computed via trigger when the verification status is VERIFICATION_SUCCESS. Do not update through prisma. verifiedAt DateTime? /// Computed via trigger when the verification status is VERIFICATION_FAILED. Do not update through prisma. spamAt DateTime? /// Computed via trigger. Do not update through prisma. isRecentlyApproved Boolean @default(false) comments Comment[] events Event[] contactMethods ServiceContactMethod[] @relation("ServiceToContactMethod") attributes ServiceAttribute[] verificationSteps VerificationStep[] suggestions ServiceSuggestion[] internalNotes InternalServiceNote[] @relation("ServiceRecievedNotes") onEventCreatedForServices NotificationPreferences[] @relation("onEventCreatedForServices") onRootCommentCreatedForServices NotificationPreferences[] @relation("onRootCommentCreatedForServices") onVerificationChangeForServices NotificationPreferences[] @relation("onVerificationChangeForServices") Notification Notification[] affiliatedUsers ServiceUser[] @relation("ServiceUsers") @@index([listedAt]) @@index([approvedAt]) @@index([verifiedAt]) @@index([spamAt]) @@index([overallScore]) @@index([privacyScore]) @@index([trustScore]) @@index([averageUserRating]) @@index([name]) @@index([verificationStatus]) @@index([kycLevel]) @@index([createdAt]) @@index([updatedAt]) @@index([slug]) @@index([previousSlugs]) @@index([serviceVisibility]) } model ServiceContactMethod { id Int @id @default(autoincrement()) /// Only include it if you want to override the formatted value. label String? /// Including the protocol (e.g. "mailto:", "tel:", "https://") value String createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt services Service @relation("ServiceToContactMethod", fields: [serviceId], references: [id], onDelete: Cascade) serviceId Int } enum AttributeCategory { PRIVACY TRUST } enum AttributeType { GOOD BAD WARNING INFO } model Attribute { id Int @id @default(autoincrement()) slug String @unique title String /// Markdown description String privacyPoints Int @default(0) trustPoints Int @default(0) category AttributeCategory type AttributeType createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt services ServiceAttribute[] notificationPreferencesOnServiceVerificationChange NotificationPreferenceOnServiceVerificationChangeFilterFilter[] } model InternalUserNote { id Int @id @default(autoincrement()) /// Markdown content String createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt user User @relation("UserRecievedNotes", fields: [userId], references: [id], onDelete: Cascade) userId Int addedByUser User? @relation("UserAddedNotes", fields: [addedByUserId], references: [id], onDelete: SetNull) addedByUserId Int? @@index([userId]) @@index([addedByUserId]) @@index([createdAt]) } model InternalServiceNote { id Int @id @default(autoincrement()) /// Markdown content String createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt service Service @relation("ServiceRecievedNotes", fields: [serviceId], references: [id], onDelete: Cascade) serviceId Int addedByUser User? @relation("UserAddedServiceNotes", fields: [addedByUserId], references: [id], onDelete: SetNull) addedByUserId Int? @@index([serviceId]) @@index([addedByUserId]) @@index([createdAt]) } model User { id Int @id @default(autoincrement()) name String @unique displayName String? link String? picture String? spammer Boolean @default(false) verified Boolean @default(false) admin Boolean @default(false) moderator Boolean @default(false) verifiedLink String? secretTokenHash String @unique feedId String @unique @default(cuid(2)) /// Computed via trigger. Do not update through prisma. totalKarma Int @default(0) /// Date when user is scheduled for deletion due to inactivity scheduledDeletionAt DateTime? @db.Date createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt lastLoginAt DateTime @default(now()) comments Comment[] karmaTransactions KarmaTransaction[] grantedKarmaTransactions KarmaTransaction[] @relation("KarmaGrantedBy") commentVotes CommentVote[] suggestions ServiceSuggestion[] suggestionMessages ServiceSuggestionMessage[] internalNotes InternalUserNote[] @relation("UserRecievedNotes") addedInternalNotes InternalUserNote[] @relation("UserAddedNotes") addedServiceNotes InternalServiceNote[] @relation("UserAddedServiceNotes") verificationRequests ServiceVerificationRequest[] notifications Notification[] @relation("NotificationOwner") notificationPreferences NotificationPreferences? serviceAffiliations ServiceUser[] @relation("UserServices") pushSubscriptions PushSubscription[] @@index([createdAt]) @@index([totalKarma]) } model CommentVote { id Int @id @default(autoincrement()) downvote Boolean @default(false) // false = upvote, true = downvote comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) commentId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int createdAt DateTime @default(now()) @@unique([commentId, userId]) // Ensure one vote per user per comment @@index([commentId]) @@index([userId]) } model ServiceAttribute { service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade) serviceId Int attribute Attribute @relation(fields: [attributeId], references: [id], onDelete: Cascade) attributeId Int createdAt DateTime @default(now()) @@id([serviceId, attributeId]) } model KarmaTransaction { id Int @id @default(autoincrement()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int action KarmaTransactionAction points Int @default(0) comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade) commentId Int? suggestion ServiceSuggestion? @relation(fields: [suggestionId], references: [id], onDelete: Cascade) suggestionId Int? description String processed Boolean @default(false) createdAt DateTime @default(now()) grantedBy User? @relation("KarmaGrantedBy", fields: [grantedByUserId], references: [id], onDelete: SetNull) grantedByUserId Int? Notification Notification[] @@index([createdAt]) @@index([userId]) @@index([processed]) @@index([suggestionId]) @@index([commentId]) @@index([grantedByUserId]) } enum VerificationStepStatus { PENDING IN_PROGRESS PASSED FAILED WARNING } model VerificationStep { id Int @id @default(autoincrement()) title String description String status VerificationStepStatus @default(PENDING) evidenceMd String? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade) serviceId Int @@index([serviceId]) @@index([status]) @@index([createdAt]) } model Category { id Int @id @default(autoincrement()) name String @unique namePluralLong String? icon String slug String @unique createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt services Service[] @relation("ServiceToCategory") notificationPreferencesOnServiceVerificationChange NotificationPreferenceOnServiceVerificationChangeFilterFilter[] @@index([name]) @@index([slug]) } model ServiceVerificationRequest { id Int @id @default(autoincrement()) service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade) serviceId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@unique([serviceId, userId]) @@index([serviceId]) @@index([userId]) @@index([createdAt]) } model ServiceScoreRecalculationJob { id Int @id @default(autoincrement()) serviceId Int @unique createdAt DateTime @default(now()) processedAt DateTime? @updatedAt @@index([processedAt]) @@index([createdAt]) } model ServiceUser { id Int @id @default(autoincrement()) userId Int user User @relation("UserServices", fields: [userId], references: [id], onDelete: Cascade) serviceId Int service Service @relation("ServiceUsers", fields: [serviceId], references: [id], onDelete: Cascade) role ServiceUserRole createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([userId, serviceId]) @@index([userId]) @@index([serviceId]) @@index([role]) } model Announcement { id Int @id @default(autoincrement()) content String type AnnouncementType link String? linkText String? startDate DateTime endDate DateTime? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@index([isActive, startDate, endDate]) } model PushSubscription { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) endpoint String @unique /// Public key for encryption p256dh String /// Authentication secret auth String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) @@index([endpoint]) }