generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum AuthMethod { COOKIE PASSKEY } enum PostType { FEATURE_REQUEST BUG_REPORT } model Board { id String @id @default(cuid()) slug String @unique name String description String? externalUrl String? iconName String? iconColor String? isArchived Boolean @default(false) voteBudget Int @default(10) voteBudgetReset String @default("monthly") lastBudgetReset DateTime? allowMultiVote Boolean @default(false) rssEnabled Boolean @default(true) rssFeedCount Int @default(50) staleDays Int @default(0) position Int @default(0) sensitivityLevel String @default("normal") velocityThreshold Int? quarantined Boolean @default(false) requireVoteVerification Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt posts Post[] activityEvents ActivityEvent[] pushSubscriptions PushSubscription[] statusConfig BoardStatus[] templates BoardTemplate[] changelogEntries ChangelogEntry[] baseline BoardBaseline? brigadePatterns BrigadePattern[] } model BoardStatus { id String @id @default(cuid()) boardId String status String label String color String position Int @default(0) enabled Boolean @default(true) board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) @@unique([boardId, status]) @@index([boardId, position]) } model User { id String @id @default(cuid()) authMethod AuthMethod @default(COOKIE) tokenHash String? @unique username String? usernameIdx String? @unique displayName String? avatarPath String? darkMode String @default("system") firstActionType String? firstActionAt DateTime? actionDiversityScore Float @default(0) voteTimingStdDev Float? boardInteractionCount Int @default(0) flagCount Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt passkeys Passkey[] posts Post[] comments Comment[] reactions Reaction[] votes Vote[] notifications Notification[] pushSubscriptions PushSubscription[] attachments Attachment[] adminLink AdminUser? edits EditHistory[] recoveryCode RecoveryCode? } model RecoveryCode { id String @id @default(cuid()) codeHash String phraseIdx String @unique userId String @unique expiresAt DateTime createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([expiresAt]) } model Passkey { id String @id @default(cuid()) credentialId String credentialIdIdx String @unique credentialPublicKey Bytes counter BigInt credentialDeviceType String credentialBackedUp Boolean transports String? userId String createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model Post { id String @id @default(cuid()) type PostType title String description Json status String @default("OPEN") statusReason String? category String? templateId String? voteCount Int @default(0) viewCount Int @default(0) isPinned Boolean @default(false) isEditLocked Boolean @default(false) isThreadLocked Boolean @default(false) isVotingLocked Boolean @default(false) frozenAt DateTime? votesVisibleAfter DateTime? onBehalfOf String? boardId String authorId String lastActivityAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) author User @relation(fields: [authorId], references: [id], onDelete: Cascade) statusChanges StatusChange[] comments Comment[] votes Vote[] adminResponses AdminResponse[] adminNotes AdminNote[] notifications Notification[] activityEvents ActivityEvent[] pushSubscriptions PushSubscription[] tags PostTag[] attachments Attachment[] editHistory EditHistory[] voteSnapshots PostVoteSnapshot[] @@index([boardId, status]) } model StatusChange { id String @id @default(cuid()) postId String fromStatus String toStatus String reason String? changedBy String createdAt DateTime @default(now()) post Post @relation(fields: [postId], references: [id], onDelete: Cascade) } model Comment { id String @id @default(cuid()) body String postId String authorId String replyToId String? isAdmin Boolean @default(false) isEditLocked Boolean @default(false) adminUserId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt post Post @relation(fields: [postId], references: [id], onDelete: Cascade) author User @relation(fields: [authorId], references: [id], onDelete: Cascade) replyTo Comment? @relation("CommentReplies", fields: [replyToId], references: [id], onDelete: SetNull) replies Comment[] @relation("CommentReplies") adminUser AdminUser? @relation(fields: [adminUserId], references: [id], onDelete: SetNull) reactions Reaction[] attachments Attachment[] editHistory EditHistory[] @@index([replyToId]) @@index([postId, createdAt]) } model Reaction { id String @id @default(cuid()) emoji String commentId String userId String createdAt DateTime @default(now()) comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([commentId, userId, emoji]) } model Vote { id String @id @default(cuid()) weight Int @default(1) importance String? phantom Boolean @default(false) voterIp String? referrer String? postId String voterId String budgetPeriod String createdAt DateTime @default(now()) post Post @relation(fields: [postId], references: [id], onDelete: Cascade) voter User @relation(fields: [voterId], references: [id], onDelete: Cascade) @@unique([postId, voterId]) } enum TeamRole { SUPER_ADMIN ADMIN MODERATOR } model AdminUser { id String @id @default(cuid()) email String? @unique passwordHash String? role TeamRole @default(ADMIN) displayName String? teamTitle String? linkedUserId String? @unique invitedById String? createdAt DateTime @default(now()) linkedUser User? @relation(fields: [linkedUserId], references: [id], onDelete: SetNull) invitedBy AdminUser? @relation("TeamInviter", fields: [invitedById], references: [id], onDelete: SetNull) invitees AdminUser[] @relation("TeamInviter") responses AdminResponse[] notes AdminNote[] comments Comment[] createdInvites TeamInvite[] @relation("InviteCreator") claimedInvite TeamInvite? @relation("InviteClaimer") } model TeamInvite { id String @id @default(cuid()) tokenHash String @unique role TeamRole label String? expiresAt DateTime createdById String claimedById String? @unique claimedAt DateTime? recoveryHash String? recoveryIdx String? createdAt DateTime @default(now()) createdBy AdminUser @relation("InviteCreator", fields: [createdById], references: [id], onDelete: Cascade) claimedBy AdminUser? @relation("InviteClaimer", fields: [claimedById], references: [id], onDelete: SetNull) @@index([expiresAt]) } model AdminResponse { id String @id @default(cuid()) body String postId String adminId String createdAt DateTime @default(now()) post Post @relation(fields: [postId], references: [id], onDelete: Cascade) admin AdminUser @relation(fields: [adminId], references: [id], onDelete: Cascade) } model ActivityEvent { id String @id @default(cuid()) type String boardId String postId String? metadata Json createdAt DateTime @default(now()) board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) post Post? @relation(fields: [postId], references: [id], onDelete: SetNull) @@index([boardId, createdAt]) @@index([createdAt]) } model PushSubscription { id String @id @default(cuid()) endpoint String endpointIdx String @unique keysP256dh String keysAuth String userId String boardId String? postId String? failureCount Int @default(0) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) board Board? @relation(fields: [boardId], references: [id], onDelete: Cascade) post Post? @relation(fields: [postId], references: [id], onDelete: SetNull) } model Category { id String @id @default(cuid()) name String @unique slug String @unique createdAt DateTime @default(now()) } model Notification { id String @id @default(cuid()) type String title String body String postId String? userId String read Boolean @default(false) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) post Post? @relation(fields: [postId], references: [id], onDelete: SetNull) @@index([userId, read, createdAt]) } model Webhook { id String @id @default(cuid()) url String secret String events String[] @default(["status_changed", "post_created", "comment_added"]) active Boolean @default(true) createdAt DateTime @default(now()) } model PostMerge { id String @id @default(cuid()) sourcePostId String targetPostId String mergedBy String createdAt DateTime @default(now()) @@index([sourcePostId]) } model ChangelogEntry { id String @id @default(cuid()) title String body String boardId String? publishedAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt board Board? @relation(fields: [boardId], references: [id], onDelete: Cascade) @@index([boardId, publishedAt]) } model AdminNote { id String @id @default(cuid()) body String postId String adminId String createdAt DateTime @default(now()) post Post @relation(fields: [postId], references: [id], onDelete: Cascade) admin AdminUser @relation(fields: [adminId], references: [id], onDelete: Cascade) } model Tag { id String @id @default(cuid()) name String @unique color String @default("#6366F1") createdAt DateTime @default(now()) posts PostTag[] } model PostTag { postId String tagId String post Post @relation(fields: [postId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@id([postId, tagId]) } model Attachment { id String @id @default(cuid()) filename String path String size Int mimeType String postId String? commentId String? uploaderId String createdAt DateTime @default(now()) post Post? @relation(fields: [postId], references: [id], onDelete: Cascade) comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade) uploader User @relation(fields: [uploaderId], references: [id], onDelete: Cascade) } model BoardTemplate { id String @id @default(cuid()) boardId String name String fields Json isDefault Boolean @default(false) position Int @default(0) board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) @@index([boardId, position]) } model SiteSettings { id String @id @default("default") appName String @default("Echoboard") logoUrl String? faviconUrl String? accentColor String @default("#F59E0B") headerFont String? bodyFont String? poweredByVisible Boolean @default(true) customCss String? ogImageUrl String? ogDescription String? updatedAt DateTime @updatedAt } model BlockedToken { id String @id @default(cuid()) tokenHash String @unique expiresAt DateTime createdAt DateTime @default(now()) @@index([expiresAt]) } model EditHistory { id String @id @default(cuid()) postId String? commentId String? editedBy String? previousTitle String? previousDescription Json? previousBody String? createdAt DateTime @default(now()) post Post? @relation(fields: [postId], references: [id], onDelete: Cascade) comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade) editor User? @relation(fields: [editedBy], references: [id], onDelete: SetNull) @@index([postId, createdAt]) @@index([commentId, createdAt]) } model Plugin { id String @id @default(cuid()) name String @unique version String description String? author String? enabled Boolean @default(false) dirPath String entryPoint String @default("index.js") config Json @default("{}") configSchema Json @default("[]") installedAt DateTime @default(now()) updatedAt DateTime @updatedAt data PluginData[] } model PluginData { id String @id @default(cuid()) pluginId String key String value Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt plugin Plugin @relation(fields: [pluginId], references: [id], onDelete: Cascade) @@unique([pluginId, key]) @@index([pluginId]) } model AnomalyEvent { id String @id @default(cuid()) type String severity String targetType String targetId String boardId String? metadata Json @default("{}") status String @default("pending") createdAt DateTime @default(now()) @@index([status, createdAt]) @@index([targetType, targetId]) @@index([boardId, createdAt]) } model BoardBaseline { id String @id @default(cuid()) boardId String @unique avgVotesPerHour Float @default(0) avgPostsPerDay Float @default(0) avgReactionsPerHour Float @default(0) peakHourOfDay Int @default(12) peakDayOfWeek Int @default(2) updatedAt DateTime @updatedAt board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) } model PostVoteSnapshot { id String @id @default(cuid()) postId String voteCount Int snapshotAt DateTime @default(now()) post Post @relation(fields: [postId], references: [id], onDelete: Cascade) @@index([postId, snapshotAt]) } model BrigadePattern { id String @id @default(cuid()) boardId String? features Json matchCount Int @default(0) createdAt DateTime @default(now()) board Board? @relation(fields: [boardId], references: [id], onDelete: Cascade) } model AdminWebhookConfig { id String @id @default(cuid()) url String events String[] active Boolean @default(true) }