// Prisma Schema for OpenFrame - Video Feedback Platform generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" } // ============================================ // AUTH MODELS (NextAuth.js compatible) // ============================================ model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? password String? // Hashed password for email/password auth createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations accounts Account[] sessions Session[] ownedWorkspaces Workspace[] workspaceMemberships WorkspaceMember[] projects Project[] comments Comment[] projectMemberships ProjectMember[] notificationSetting NotificationSetting? watchProgress WatchProgress[] @@map("users") } enum DownloadEgressSource { ORIGINAL COMPRESSED } model DownloadEgressEvent { id String @id @default(cuid()) versionId String videoId String projectId String workspaceId String billedUserId String downloaderUserId String? source DownloadEgressSource quality Int? estimatedBytes BigInt @default(0) createdAt DateTime @default(now()) @@index([billedUserId, createdAt]) @@index([workspaceId, createdAt]) @@index([versionId, createdAt]) @@map("download_egress_events") } model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@map("accounts") } model Session { id String @id @default(cuid()) sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("sessions") } model VerificationToken { identifier String token String @unique expires DateTime @@unique([identifier, token]) @@map("verification_tokens") } // ============================================ // APPLICATION MODELS // ============================================ // ---- Workspace ---- model Workspace { id String @id @default(cuid()) name String slug String @unique description String? @db.Text // Ownership ownerId String owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations members WorkspaceMember[] projects Project[] @@index([ownerId]) @@index([slug]) @@map("workspaces") } model WorkspaceMember { id String @id @default(cuid()) role WorkspaceMemberRole @default(COMMENTATOR) workspaceId String workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @@unique([workspaceId, userId]) @@index([userId]) @@map("workspace_members") } enum WorkspaceMemberRole { ADMIN // Full access: manage members, delete projects, etc. COMMENTATOR // Can view all projects and comment only } // ---- Project ---- model Project { id String @id @default(cuid()) name String description String? @db.Text slug String @unique // URL-friendly identifier // Visibility visibility ProjectVisibility @default(PRIVATE) // Ownership ownerId String owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) // Workspace (required - every project belongs to a workspace) workspaceId String workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations videos Video[] members ProjectMember[] shareLinks ShareLink[] commentTags CommentTag[] @@index([ownerId]) @@index([slug]) @@index([workspaceId]) @@index([workspaceId, updatedAt(sort: Desc)]) @@map("projects") } enum ProjectVisibility { PRIVATE // Only owner can access INVITE // Owner + specifically invited members PUBLIC // Anyone with the link can access } model ProjectMember { id String @id @default(cuid()) role ProjectMemberRole @default(COMMENTATOR) projectId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @@unique([projectId, userId]) @@index([userId]) @@map("project_members") } enum ProjectMemberRole { ADMIN // Can manage members, settings, delete videos COMMENTATOR // Can view and comment only } model Video { id String @id @default(cuid()) title String description String? @db.Text // Ordering within project position Int @default(0) // Project relation projectId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations versions VideoVersion[] shareLinks ShareLink[] @@index([projectId]) @@map("videos") } model VideoVersion { id String @id @default(cuid()) versionNumber Int // 1, 2, 3... versionLabel String? // Optional label like "Final Cut", "Review Draft" // Video source (future-proof for multiple providers) providerId String // 'youtube', 'vimeo', 'direct' videoId String // Provider-specific video ID originalUrl String // Original URL submitted by user // Cached metadata title String? thumbnailUrl String? duration Int? // Duration in seconds // Status isActive Boolean @default(true) // Currently displayed version // Parent video videoParentId String video Video @relation(fields: [videoParentId], references: [id], onDelete: Cascade) // Timestamps createdAt DateTime @default(now()) // Relations comments Comment[] watchProgress WatchProgress[] @@unique([videoParentId, versionNumber]) @@index([videoParentId]) @@index([videoParentId, isActive]) @@map("video_versions") } model Comment { id String @id @default(cuid()) // Comment content content String? @db.Text // Text content (null if voice-only) // Timestamp in video (in seconds, with decimal for precision) timestamp Float // e.g., 65.5 = 1:05.5 timestampEnd Float? // Optional end timestamp for range comments // Voice recording (optional) voiceUrl String? // URL to voice recording file voiceDuration Float? // Duration of voice recording in seconds // Image attachment (optional) imageUrl String? // URL to uploaded image file // Annotation drawing data (JSON string of strokes) annotationData String? @db.Text // Threading parentId String? parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade) replies Comment[] @relation("CommentReplies") // Status isResolved Boolean @default(false) resolvedAt DateTime? // Author (optional for guest comments) authorId String? author User? @relation(fields: [authorId], references: [id], onDelete: SetNull) // Guest author info (when authorId is null) guestName String? guestEmail String? // Video version relation versionId String version VideoVersion @relation(fields: [versionId], references: [id], onDelete: Cascade) // Comment tag (colored category) tagId String? tag CommentTag? @relation(fields: [tagId], references: [id], onDelete: SetNull) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([versionId]) @@index([parentId]) @@index([authorId]) @@index([timestamp]) @@index([tagId]) @@index([versionId, isResolved, timestamp]) @@index([versionId, parentId, createdAt]) @@map("comments") } model CommentTag { id String @id @default(cuid()) name String // e.g., "Feedback", "Technical", "Urgent" color String // Hex color, e.g., "#3B82F6" // Project relation (tags are per-project) projectId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) // Position for ordering in UI position Int @default(0) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations comments Comment[] @@unique([projectId, name]) @@index([projectId]) @@map("comment_tags") } model ShareLink { id String @id @default(cuid()) token String @unique // Random token for URL // What is being shared projectId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) videoId String? video Video? @relation(fields: [videoId], references: [id], onDelete: Cascade) // Permissions permission SharePermission @default(VIEW) // Optional restrictions expiresAt DateTime? // Link expiration passwordHash String? // Bcrypt hash of optional password protection // Settings allowGuests Boolean @default(true) // Allow comments without account // Timestamps createdAt DateTime @default(now()) @@index([projectId]) @@index([videoId]) @@index([projectId, videoId]) @@unique([projectId, videoId, permission]) @@index([token]) @@index([token, expiresAt]) @@map("share_links") } enum SharePermission { VIEW // Can only view COMMENT // Can view and comment } // ============================================ // NOTIFICATION SETTINGS // ============================================ model NotificationSetting { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) // Telegram webhook telegramBotToken String? // Bot token from @BotFather telegramChatId String? // Chat/group ID to send messages to telegramEnabled Boolean @default(false) // Email notifications (uses account email by default) emailEnabled Boolean @default(false) // Event subscriptions onNewVideo Boolean @default(true) onNewVersion Boolean @default(true) onNewComment Boolean @default(true) onNewReply Boolean @default(true) // User timezone for notification timestamps (IANA timezone identifier) timezone String @default("UTC") // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("notification_settings") } // ============================================ // WATCH PROGRESS // ============================================ model WatchProgress { id String @id @default(cuid()) // User relation (optional for guest progress, though typically requires auth) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) // Video version relation versionId String version VideoVersion @relation(fields: [versionId], references: [id], onDelete: Cascade) // Progress data progress Float // Current playback position in seconds duration Float // Total video duration at time of save percentage Float // Progress as percentage (0-100) // Timestamps updatedAt DateTime @updatedAt createdAt DateTime @default(now()) // One progress record per user per version @@unique([userId, versionId]) @@index([userId]) @@index([versionId]) @@map("watch_progress") } // Rate limiting table (created as UNLOGGED via raw SQL migration) // Defined here so `prisma db push` doesn't drop it model RateLimit { id Int @id @default(autoincrement()) key String @db.VarChar(255) action String @db.VarChar(50) count Int @default(1) windowStart DateTime @default(now()) @map("window_start") @@unique([key, action]) @@index([key, action]) @@index([windowStart]) @@map("rate_limits") }