MicProtocol / apps /api /prisma /schema.prisma
Benaah's picture
Upload folder using huggingface_hub
6abfd9d verified
Raw History Blame Contribute Delete
126 kB
// ─── MicProtocol Prisma Schema ───────────────────────── // Domain-driven schema for podcast guest management generator client { provider = "prisma-client-js" previewFeatures = [] } datasource db { provider = "postgresql" } // ─── Enums ─────────────────────────────────────────── enum GuestStatus { prospect contacted scheduled recorded published } enum GuestJourneyEventType { created contacted scheduled recorded published follow_up milestone note } enum EnergyPreference { morning afternoon evening flexible } enum EpisodeStatus { draft scheduled recorded editing published } enum OutreachMessageType { initial followup booking reminder } enum OutreachSequenceStatus { active paused completed } enum AssetRequestType { headshot bio social_links intro_audio talking_points } enum AssetSubmissionStatus { pending approved rejected } enum RecordingPlatform { riverside zoom studio micprotocol_native } enum DiagnosticStatus { pending passed warning failed } enum DiagnosticType { rt60 emi_hum storage network_jitter vad_mute } enum CodecType { wav opus } enum TrackStatus { waiting recording uploading completed error } enum AudioChunkStatus { local_cached uploading server_saved failed } enum WorkspaceRole { owner admin editor viewer } enum ClipType { viral intro quote cta } enum ExportPlatform { tiktok instagram youtube linkedin twitter } enum ExportJobStatus { queued rendering completed failed } enum NotificationType { system booking message alert outreach asset episode badge trust_score verification network_booking recording billing export clip digest } enum PostInterviewWorkflowStatus { pending running completed failed } enum PostInterviewTaskType { send_recording_link request_testimonial provide_clips schedule_check_in track_promotion } enum PostInterviewTaskStatus { pending in_progress completed failed } // ─── Portal Token (for guest access to upload/booking portal) ─ // These tokens are cryptographically random and time-limited, // unlike the previous approach that used episode UUIDs directly. model PortalToken { id String @id @default(cuid()) token String @unique // crypto-random 32+ byte hex string episodeId String @map("episode_id") @db.Uuid expiresAt DateTime @map("expires_at") createdAt DateTime @default(now()) @map("created_at") usedAt DateTime? @map("used_at") episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade) @@index([token]) @@index([episodeId]) @@map("portal_tokens") } // ─── User ──────────────────────────────────────────── model User { id String @id @default(uuid()) @db.Uuid name String? email String @unique @db.Citext emailVerified Boolean @default(false) @map("email_verified") image String? passwordHash String? @map("password_hash") profile Json @default("{}") defaultTimezone String @default("UTC") @map("default_timezone") isActive Boolean @default(true) @map("is_active") isAdmin Boolean @default(false) @map("is_admin") // Platform RBAC: role + per-user permission overrides (JSON). adminRole is // the baseline; adminPermissions merges/adds/subtracts on top of it. adminRole AdminRole? @map("admin_role") adminPermissions Json @default("{}") @map("admin_permissions") // Suspension: when set, the user cannot authenticate or perform actions. suspendedAt DateTime? @map("suspended_at") suspendedReason String? @map("suspended_reason") suspendedBy String? @map("suspended_by") @db.Uuid lastLoginAt DateTime? @map("last_login_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") memberships WorkspaceMember[] guestPublicProfile GuestPublicProfile? notifications Notification[] webPushSubscriptions WebPushSubscription[] sessions Session[] accounts Account[] diaries Diary[] profileSettings UserProfileSettings? notificationPrefs UserNotificationPreferences? securitySettings UserSecuritySettings? conversationMembers ConversationMember[] messages ConversationMessage[] reactions MessageReaction[] mediaComments MediaComment[] mediaCommentsResolved MediaComment[] @relation("MediaCommentResolver") tasksAssigned Task[] @relation("TaskAssignee") tasksCreated Task[] @relation("TaskCreator") verificationsAuthored WorkspaceVerification[] @relation("VerificationReviewer") orders Order[] // Service ratings (user's ratings for platform services) serviceRatings ServiceRating[] // Surveys created by this user createdSurveys Survey[] @relation("SurveyCreator") // Survey responses submitted by this user surveyResponses SurveyResponse[] // Support tickets created by this user supportTickets SupportTicket[] @relation("SupportTicketUser") // Support messages authored by this user supportMessages SupportMessage[] // Support agent assignments made to/by this user supportAssignments SupportAgentAssignment[] // Tickets where this user is the assigned agent supportTicketsAssigned SupportTicket[] @relation("SupportAssignee") // CSAT ratings submitted by this user supportRatings SupportSatisfactionRating[] // Platform governance relations (reverse side of admin-owned relations) platformSettingsUpdated PlatformSetting[] featureFlagsUpdated FeatureFlag[] abuseReportsAsReporter AbuseReport[] @relation("AbuseReportReporter") abuseReportsAsHandler AbuseReport[] @relation("AbuseReportHandler") adminNotes AdminNote[] announcements Announcement[] supervisionRunsTriggered SupervisionRun[] supervisionFindingsAssigned SupervisionFinding[] // Document management relations documents Document[] documentVersions DocumentVersion[] @relation("DocumentVersionUploader") documentAccessLogs DocumentAccessLog[] securityBlocklists SecurityBlocklist[] securityFlags SecurityFlag[] @@map("users") } // ─── Audit Log (SOC2 CC6) ──────────────────────────── // Append-only audit trail of write operations. Best-effort writes — // audit failures never block the request path. model AuditLog { id String @id @default(uuid()) @db.Uuid userId String? @map("user_id") @db.Uuid workspaceId String? @map("workspace_id") @db.Uuid action String entityType String? entityId String? metadata Json @default("{}") @map("metadata") ipAddress String? @map("ip_address") userAgent String? @map("user_agent") entryHash String? @map("entry_hash") previousHash String? @map("previous_hash") createdAt DateTime @default(now()) @map("created_at") @@index([workspaceId, createdAt]) @@index([userId]) @@index([createdAt]) @@index([action]) @@map("audit_logs") } // ─── Better Auth: Session ──────────────────────────── model Session { id String @id @default(uuid()) expiresAt DateTime @map("expires_at") token String @unique ipAddress String? @map("ip_address") userAgent String? @map("user_agent") userId String @map("user_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([token]) @@map("sessions") } // ─── Better Auth: Account (OAuth providers) ────────── model Account { id String @id @default(uuid()) accountId String @map("account_id") providerId String @map("provider_id") userId String @map("user_id") @db.Uuid accessToken String? @map("access_token") refreshToken String? @map("refresh_token") idToken String? @map("id_token") accessTokenExpiresAt DateTime? @map("access_token_expires_at") refreshTokenExpiresAt DateTime? @map("refresh_token_expires_at") scope String? password String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@map("accounts") } // ─── Better Auth: Verification (email tokens, etc.) ── model Verification { id String @id @default(uuid()) identifier String value String expiresAt DateTime @map("expires_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@unique([identifier, value]) @@map("verifications") } // ─── Better Auth: JWKS (JWT Key Management) ────────── model Jwks { id String @id @default(uuid()) publicKey String @map("public_key") @db.Text privateKey String @map("private_key") @db.Text createdAt DateTime @default(now()) @map("created_at") expiresAt DateTime? @map("expires_at") alg String? crv String? @@map("jwks") } // ─── Notification ──────────────────────────────────── model Notification { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid type NotificationType @default(system) title String message String @db.Text isRead Boolean @default(false) @map("is_read") link String? metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, isRead]) @@index([createdAt]) @@map("notifications") } // ─── Workspace ─────────────────────────────────────── model Workspace { id String @id @default(uuid()) @db.Uuid name String slug String @unique subscriptionPlanId SubscriptionPlanId @default(hobby) @map("subscription_plan_id") billingProvider BillingProvider @default(stripe) @map("billing_provider") subscriptionStatus SubscriptionStatus @default(active) @map("subscription_status") billingSettings Json @default("{}") @map("billing_settings") // Suspension: when suspended, the workspace cannot publish, record, or // transact. Slug remains reserved (not released for squatting). suspendedAt DateTime? @map("suspended_at") suspendedReason String? @map("suspended_reason") // Anti-squatting identity status. Drives public-listing eligibility, // network search ranking, and feature gating. See docs/workspace-verification.md. verificationStatus WorkspaceVerificationStatus @default(unverified) @map("verification_status") // Auto-flagged heuristics at the time of slug approval. Surfaced in // the admin review queue. Cleared when status moves to VERIFIED/FEATURED. verificationFlags WorkspaceVerificationReason[] @default([]) @map("verification_flags") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") members WorkspaceMember[] podcasts Podcast[] diaries Diary[] brandingSettings WorkspaceBrandingSettings? workspaceSettings WorkspaceSettings? integrations WorkspaceIntegration[] conversations Conversation[] tasks Task[] mediaComments MediaComment[] events Event[] wallet Wallet? recordingDevices RecordingDevice[] // Verification pipeline relations verification WorkspaceVerification? brandClaim BrandProtection? @relation("BrandClaim") redemptionsAsPrevious SlugRedemption[] @relation("SlugRedemptionPrevious") redemptionsAsReclaimed SlugRedemption[] @relation("SlugRedemptionReclaimed") // Support models supportTickets SupportTicket[] supportCategories SupportCategoryConfig[] supportSLAs SupportSLA[] cannedResponses CannedResponse[] webhookEndpoints WebhookEndpoint[] // Document management relations documents Document[] documentShares DocumentShare[] documentAccessLogs DocumentAccessLog[] securityFlags SecurityFlag[] @@index([verificationStatus]) @@index([updatedAt]) @@map("workspaces") } // ─── Workspace Verification Record ───────────────────── // // One-to-one with Workspace. Stores evidence submitted by the owner // (RSS feed, podcast URL, domain, identity doc) plus the review trail. // Kept separate from Workspace to keep the hot workspace table lean // and to avoid bloating writes during routine workspace updates. model WorkspaceVerification { id String @id @default(uuid()) @db.Uuid workspaceId String @unique @map("workspace_id") @db.Uuid status WorkspaceVerificationStatus @default(unverified) @map("status") // Submitted evidence rssFeedUrl String? @map("rss_feed_url") podcastUrl String? @map("podcast_url") // Apple Podcasts / Spotify / YouTube domain String? @map("domain") // e.g. techtalks.show socialLinks Json @default("{}") @map("social_links") identityDocKey String? @map("identity_doc_key") // R2/S3 storage key for ID upload // Verification metadata submittedAt DateTime? @map("submitted_at") verifiedAt DateTime? @map("verified_at") expiresAt DateTime? @map("expires_at") // For time-bound verification reviewerId String? @map("reviewer_id") @db.Uuid reviewNotes String? @db.Text @map("review_notes") // Auto-detection flags captured at evaluation time autoFlags WorkspaceVerificationReason[] @default([]) @map("auto_flags") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) reviewer User? @relation("VerificationReviewer", fields: [reviewerId], references: [id], onDelete: SetNull) @@index([status]) @@index([submittedAt]) @@map("workspace_verifications") } // ─── Brand Protection ───────────────────────────────── // // Curated database of well-known podcast brands. Any workspace slug that // matches one of these MUST go through manual verification before // claiming the subdomain. // // Populated by: // • A seed script with the top ~200 English-language podcasts // • Periodic ingestion from Apple Podcasts / Spotify directories // • Admin manual additions for trademark-protected names // // `claimedByWorkspaceId` is set once the legitimate owner has been // verified — at that point new attempts to register the same slug will // fail with a "claimed by another workspace" error. model BrandProtection { id String @id @default(uuid()) @db.Uuid slug String @unique // Normalized lowercase form brandName String @map("brand_name") // Original capitalization category BrandProtectionCategory? // External identifiers (for cross-platform verification) applePodcastId String? @map("apple_podcast_id") spotifyId String? @map("spotify_id") rssUrl String? @map("rss_url") websiteUrl String? @map("website_url") // Once the legitimate owner has proven control, link the workspace // here so future squat attempts are blocked outright. claimedByWorkspaceId String? @unique @map("claimed_by_workspace_id") @db.Uuid claimedAt DateTime? @map("claimed_at") notes String? @db.Text protectedAt DateTime @default(now()) @map("protected_at") updatedAt DateTime @updatedAt @map("updated_at") claimedByWorkspace Workspace? @relation("BrandClaim", fields: [claimedByWorkspaceId], references: [id], onDelete: SetNull) @@index([category]) @@map("brand_protection") } // ─── Slug Redemption Tombstone ──────────────────────── // // When a workspace is renamed or deleted, the freed slug enters a // redemption window where ONLY the previous owner can reclaim it. This // prevents squatting via "wait until someone renames and grab their // established slug" attacks. // // Window lengths (configured in WorkspaceVerificationService): // • renamed → 30 days exclusive to previous owner // • deleted → 60 days exclusive to previous owner // • expired_inactive → 30 days exclusive to previous owner // • verification_rejected → 7 days exclusive to previous owner model SlugRedemption { id String @id @default(uuid()) @db.Uuid slug String @unique previousWorkspaceId String @map("previous_workspace_id") @db.Uuid reason SlugRedemptionReason releasedAt DateTime @default(now()) @map("released_at") // During this window, only `previousWorkspaceId` can reclaim the slug. redemptionExpiresAt DateTime @map("redemption_expires_at") reclaimedAt DateTime? @map("reclaimed_at") reclaimedByWorkspaceId String? @map("reclaimed_by_workspace_id") @db.Uuid previousWorkspace Workspace @relation("SlugRedemptionPrevious", fields: [previousWorkspaceId], references: [id], onDelete: Cascade) reclaimedByWorkspace Workspace? @relation("SlugRedemptionReclaimed", fields: [reclaimedByWorkspaceId], references: [id], onDelete: SetNull) @@index([slug]) @@index([redemptionExpiresAt]) @@map("slug_redemptions") } // ─── Diary Workspace ─────────────────────────────── model Diary { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid workspaceId String @map("workspace_id") @db.Uuid title String @default("Workspace Diary") activeEntryId String? @map("active_entry_id") @db.Uuid entries Json @default("[]") workflowNodes Json @default("[]") @map("workflow_nodes") workflowEdges Json @default("[]") @map("workflow_edges") notepad String @default("") @db.Text createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@unique([userId, workspaceId]) @@index([workspaceId]) @@map("diaries") } model WorkspaceMember { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid workspaceId String @map("workspace_id") @db.Uuid role WorkspaceRole @default(editor) joinedAt DateTime @default(now()) @map("joined_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@unique([userId, workspaceId]) @@index([workspaceId]) @@map("workspace_members") } // ─── Podcast ───────────────────────────────────────── model Podcast { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid title String rssFeedUrl String? @map("rss_feed_url") recordingSettings Json @default("{}") @map("recording_settings") outreachDefaults Json @default("{}") @map("outreach_defaults") paymentSettings Json @default("{}") @map("payment_settings") calendarIntegration CalendarIntegration? calendarIntegrationId String? @map("calendar_integration_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) guests Guest[] episodes Episode[] availabilities Availability[] episodeTemplates EpisodeTemplate[] tags Tag[] sponsorInvoices SponsorInvoice[] conversations Conversation[] tasks Task[] // Network relations networkBookmarks NetworkBookmark[] networkBookingRequests NetworkBookingRequest[] profileViews ProfileView[] @relation("ViewerPodcast") @@index([workspaceId]) @@map("podcasts") } // ─── Guest ─────────────────────────────────────────── model Guest { id String @id @default(uuid()) @db.Uuid podcastId String @map("podcast_id") @db.Uuid email String @db.Citext fullName String @map("full_name") company String? title String? socialLinks Json @default("{}") @map("social_links") bio String? @db.Text status GuestStatus @default(prospect) priorityScore Int @default(50) @map("priority_score") lastContactAt DateTime? @map("last_contact_at") customFields Json @default("{}") @map("custom_fields") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") podcast Podcast @relation(fields: [podcastId], references: [id], onDelete: Cascade) guestTags GuestTag[] outreachSequences OutreachSequence[] episodes Episode[] technicalProfile GuestTechnicalProfile? journeyEvents GuestJourneyEvent[] conversations Conversation[] supportTickets SupportTicket[] @relation("SupportTicketGuest") availabilities Availability[] @@unique([podcastId, email]) @@index([podcastId, status]) @@index([podcastId, priorityScore]) @@index([status, lastContactAt]) // For follow-up queries @@index([podcastId, status, lastContactAt]) // For podcast-specific follow-ups @@index([podcastId, createdAt]) // For recent guests lists @@map("guests") } model GuestTechnicalProfile { id String @id @default(uuid()) @db.Uuid guestId String @unique @map("guest_id") @db.Uuid recordingPlatform RecordingPlatform @default(zoom) @map("recording_platform") microphoneExperienceLevel Int @default(3) @map("microphone_experience_level") internetReliabilityScore Int @default(3) @map("internet_reliability_score") techSetupNotes String? @map("tech_setup_notes") @db.Text preferredEnergyWindow EnergyPreference @default(flexible) @map("preferred_energy_window") prefersAsyncInterview Boolean @default(false) @map("prefers_async_interview") timezone String @default("UTC") metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") guest Guest @relation(fields: [guestId], references: [id], onDelete: Cascade) @@index([recordingPlatform, preferredEnergyWindow]) @@map("guest_technical_profiles") } model GuestJourneyEvent { id String @id @default(uuid()) @db.Uuid guestId String @map("guest_id") @db.Uuid eventType GuestJourneyEventType @map("event_type") title String details String? @db.Text eventAt DateTime @default(now()) @map("event_at") metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") guest Guest @relation(fields: [guestId], references: [id], onDelete: Cascade) @@index([guestId, eventAt]) @@index([guestId, eventType]) @@map("guest_journey_events") } // ─── Tags ──────────────────────────────────────────── model Tag { id String @id @default(uuid()) @db.Uuid podcastId String @map("podcast_id") @db.Uuid name String color String @default("#6C5CE7") createdAt DateTime @default(now()) @map("created_at") podcast Podcast @relation(fields: [podcastId], references: [id], onDelete: Cascade) guestTags GuestTag[] @@unique([podcastId, name]) @@map("tags") } model GuestTag { id String @id @default(uuid()) @db.Uuid guestId String @map("guest_id") @db.Uuid tagId String @map("tag_id") @db.Uuid addedAt DateTime @default(now()) @map("added_at") guest Guest @relation(fields: [guestId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@unique([guestId, tagId]) @@index([tagId]) @@map("guest_tags") } // ─── Outreach ──────────────────────────────────────── model OutreachSequence { id String @id @default(uuid()) @db.Uuid guestId String @map("guest_id") @db.Uuid status OutreachSequenceStatus @default(active) templateId String? @map("template_id") @db.Uuid currentStep Int @default(0) @map("current_step") startedAt DateTime @default(now()) @map("started_at") updatedAt DateTime @updatedAt @map("updated_at") guest Guest @relation(fields: [guestId], references: [id], onDelete: Cascade) messages OutreachMessage[] @@index([guestId, status]) @@map("outreach_sequences") } model OutreachMessage { id String @id @default(uuid()) @db.Uuid sequenceId String @map("sequence_id") @db.Uuid type OutreachMessageType subject String bodyHtml String @map("body_html") @db.Text bodyText String @map("body_text") @db.Text sentAt DateTime? @map("sent_at") openedAt DateTime? @map("opened_at") clickedAt DateTime? @map("clicked_at") metadata Json @default("{}") @map("metadata") createdAt DateTime @default(now()) @map("created_at") sequence OutreachSequence @relation(fields: [sequenceId], references: [id], onDelete: Cascade) @@index([sequenceId]) @@index([sequenceId, type]) @@map("outreach_messages") } // ─── Episode ───────────────────────────────────────── model Episode { id String @id @default(uuid()) @db.Uuid podcastId String @map("podcast_id") @db.Uuid guestId String @map("guest_id") @db.Uuid title String status EpisodeStatus @default(draft) scheduledAt DateTime? @map("scheduled_at") durationMinutes Int? @map("duration_minutes") hostNotes String? @map("host_notes") @db.Text guestInstructions String? @map("guest_instructions") @db.Text recordingPlatform RecordingPlatform @default(zoom) @map("recording_platform") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") podcast Podcast @relation(fields: [podcastId], references: [id], onDelete: Cascade) guest Guest @relation(fields: [guestId], references: [id], onDelete: Cascade) bookings Booking[] assetRequests AssetRequest[] promoTasks PromoTask[] transcripts Transcript[] clips Clip[] bookingHistory BookingHistory[] recordingSession RecordingSession? postInterviewWorkflow PostInterviewWorkflow? portalTokens PortalToken[] conversations Conversation[] mediaComments MediaComment[] tasks Task[] @@index([podcastId, status]) @@index([guestId]) @@map("episodes") } model PostInterviewWorkflow { id String @id @default(uuid()) @db.Uuid episodeId String @unique @map("episode_id") @db.Uuid status PostInterviewWorkflowStatus @default(pending) startedAt DateTime? @map("started_at") completedAt DateTime? @map("completed_at") checkInDueAt DateTime? @map("check_in_due_at") lastError String? @map("last_error") @db.Text metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade) tasks PostInterviewTask[] @@index([status]) @@index([checkInDueAt]) @@map("post_interview_workflows") } model PostInterviewTask { id String @id @default(uuid()) @db.Uuid workflowId String @map("workflow_id") @db.Uuid taskType PostInterviewTaskType @map("task_type") status PostInterviewTaskStatus @default(pending) dueAt DateTime? @map("due_at") completedAt DateTime? @map("completed_at") errorMessage String? @map("error_message") @db.Text metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workflow PostInterviewWorkflow @relation(fields: [workflowId], references: [id], onDelete: Cascade) @@unique([workflowId, taskType]) @@index([workflowId]) @@index([status, dueAt]) @@map("post_interview_tasks") } model EpisodeTemplate { id String @id @default(uuid()) @db.Uuid podcastId String @map("podcast_id") @db.Uuid name String durationMinutes Int @default(60) @map("duration_minutes") requiredAssets Json @default("[]") @map("required_assets") guestInstructions String? @map("guest_instructions") @db.Text createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") podcast Podcast @relation(fields: [podcastId], references: [id], onDelete: Cascade) @@index([podcastId]) @@map("episode_templates") } // ─── Booking ───────────────────────────────────────── model Booking { id String @id @default(uuid()) @db.Uuid episodeId String @map("episode_id") @db.Uuid calendarEventId String? @map("calendar_event_id") startTime DateTime @map("start_time") endTime DateTime @map("end_time") timezone String meetLink String? @map("meet_link") calendarProviderData Json @default("{}") @map("calendar_provider_data") isConfirmed Boolean @default(false) @map("is_confirmed") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade) @@index([episodeId]) @@index([startTime, endTime]) @@map("bookings") } // ─── Availability ────────────────────────────────────── // Defines recurring weekly availability windows for a guest within a podcast. // Used to verify that booking times fall within a guest's available hours. model Availability { id String @id @default(uuid()) @db.Uuid guestId String @map("guest_id") @db.Uuid podcastId String @map("podcast_id") @db.Uuid dayOfWeek Int @map("day_of_week") // 0=Sunday ... 6=Saturday startHour Int @map("start_hour") // 0-23 endHour Int @map("end_hour") // 0-23, must be > startHour timezone String @map("timezone") // IANA timezone for the window isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") guest Guest @relation(fields: [guestId], references: [id], onDelete: Cascade) podcast Podcast @relation(fields: [podcastId], references: [id], onDelete: Cascade) @@index([guestId, isActive]) @@index([podcastId, isActive]) @@index([guestId, dayOfWeek]) @@map("availabilities") } model CalendarIntegration { id String @id @default(uuid()) @db.Uuid podcastId String @unique @map("podcast_id") @db.Uuid provider String // "google", "outlook", etc. accessToken String @map("access_token") refreshToken String @map("refresh_token") expiresAt DateTime @map("expires_at") calendarId String? @map("calendar_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") podcast Podcast @relation(fields: [podcastId], references: [id], onDelete: Cascade) @@map("calendar_integrations") } // ─── Assets ────────────────────────────────────────── model AssetRequest { id String @id @default(uuid()) @db.Uuid episodeId String @map("episode_id") @db.Uuid type AssetRequestType isRequired Boolean @default(true) @map("is_required") instructions String? @db.Text dueDate DateTime? @map("due_date") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade) submissions AssetSubmission[] @@index([episodeId]) @@index([episodeId, type]) @@map("asset_requests") } model AssetSubmission { id String @id @default(uuid()) @db.Uuid requestId String @map("request_id") @db.Uuid fileUrl String @map("file_url") fileType String @map("file_type") fileSize Int @map("file_size") status AssetSubmissionStatus @default(pending) submittedAt DateTime @default(now()) @map("submitted_at") updatedAt DateTime @updatedAt @map("updated_at") request AssetRequest @relation(fields: [requestId], references: [id], onDelete: Cascade) @@index([requestId, status]) @@map("asset_submissions") } // ─── Promotion ─────────────────────────────────────── model PromoTask { id String @id @default(uuid()) @db.Uuid episodeId String @map("episode_id") @db.Uuid title String description String? @db.Text platform String scheduledAt DateTime? @map("scheduled_at") completedAt DateTime? @map("completed_at") metadata Json @default("{}") @map("metadata") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade) @@index([episodeId]) @@map("promo_tasks") } // ─── Recording Infrastructure ────────────────────────── model RecordingSession { id String @id @default(uuid()) @db.Uuid episodeId String @unique @map("episode_id") @db.Uuid status String @default("active") // active, completed, failed, processing startedAt DateTime? @map("started_at") endedAt DateTime? @map("ended_at") ntpStartOffsetMs Int? @map("ntp_start_offset_ms") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade) tracks RecordingTrack[] diagnostics DiagnosticRun[] @@map("recording_sessions") } model DiagnosticRun { id String @id @default(uuid()) @db.Uuid sessionId String @map("session_id") @db.Uuid guestId String? @map("guest_id") @db.Uuid // Nullable if host ran it type DiagnosticType status DiagnosticStatus resultData Json @default("{}") @map("result_data") message String? @db.Text createdAt DateTime @default(now()) @map("created_at") session RecordingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) @@index([sessionId, type]) @@map("diagnostic_runs") } model RecordingTrack { id String @id @default(uuid()) @db.Uuid sessionId String @map("session_id") @db.Uuid guestId String? @map("guest_id") @db.Uuid // Nullable if host track isHost Boolean @default(false) @map("is_host") status TrackStatus @default(waiting) codec CodecType @default(wav) sampleRate Int @default(48000) @map("sample_rate") bitDepth Int @default(24) @map("bit_depth") trackUrl String? @map("track_url") // Final mixed/processed URL durationSeconds Float? @map("duration_seconds") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") session RecordingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) chunks AudioChunk[] @@index([sessionId]) @@map("recording_tracks") } model AudioChunk { id String @id @default(uuid()) @db.Uuid trackId String @map("track_id") @db.Uuid chunkIndex Int @map("chunk_index") startTimeOffsetMs Int @map("start_time_offset_ms") endTimeOffsetMs Int @map("end_time_offset_ms") sizeBytes Int @map("size_bytes") status AudioChunkStatus @default(local_cached) fileUrl String? @map("file_url") // S3/R2 presigned URL after upload checksum String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") track RecordingTrack @relation(fields: [trackId], references: [id], onDelete: Cascade) @@unique([trackId, chunkIndex]) @@index([trackId, chunkIndex]) @@index([trackId, status]) @@map("audio_chunks") } // ─── Recording Hardware Device ───────────────────────── // Registry of hardware recording devices detected via the Web // EnumerateDevices API and used for recording track configuration. enum RecordingDeviceType { microphone interface mixer recorder } model RecordingDevice { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid deviceId String // Browser-specific MediaDeviceInfo.deviceId name String // Human-readable device label type RecordingDeviceType @default(microphone) @map("type") manufacturer String? model String? sampleRate Int? @map("sample_rate") // Hz, e.g. 44100, 48000, 96000 bitDepth Int? @map("bit_depth") // bits, e.g. 16, 24, 32 channels Int? // 1=mono, 2=stereo, etc. isActive Boolean @default(true) @map("is_active") lastSeenAt DateTime? @map("last_seen_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@index([workspaceId, isActive]) @@index([workspaceId, type]) @@unique([workspaceId, deviceId]) @@map("recording_devices") } // ─── ViralClip Engine (Content Studio) ─────────────── model Transcript { id String @id @default(uuid()) @db.Uuid episodeId String @map("episode_id") @db.Uuid fullText String @map("full_text") @db.Text segments Json @default("[]") @map("segments") // word-level timestamps speakerDiarization Json @default("{}") @map("speaker_diarization") // who spoke when language String @default("en") confidenceScore Float? @map("confidence_score") processingDuration Int? @map("processing_duration_ms") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade) @@index([episodeId]) @@map("transcripts") } model Clip { id String @id @default(uuid()) @db.Uuid episodeId String @map("episode_id") @db.Uuid startSeconds Int @map("start_seconds") endSeconds Int @map("end_seconds") type ClipType @default(viral) viralityScore Int @default(0) @map("virality_score") suggestedCaption String? @map("suggested_caption") @db.Text suggestedHook String? @map("suggested_hook") aiTags Json @default("[]") @map("ai_tags") // topics detected transcriptSlice String? @map("transcript_slice") @db.Text // the text content of this clip isApproved Boolean @default(false) @map("is_approved") // Multimodal CLIP scores (0-100, null until scored by Python engine) visualScore Int? @map("visual_score") audioScore Int? @map("audio_score") transcriptScore Int? @map("transcript_score") scoringMetadata Json? @map("scoring_metadata") // lufs, deadAir, hookStrength, etc. scoredAt DateTime? @map("scored_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade) exportJobs ExportJob[] metrics PerformanceMetric[] @@index([episodeId, viralityScore]) @@index([type]) @@map("clips") } model ExportJob { id String @id @default(uuid()) @db.Uuid clipId String @map("clip_id") @db.Uuid platform ExportPlatform status ExportJobStatus @default(queued) outputUrl String? @map("output_url") renderingParams Json @default("{}") @map("rendering_params") // colors, fonts, aspect_ratio, template errorMessage String? @map("error_message") startedAt DateTime? @map("started_at") completedAt DateTime? @map("completed_at") publishedAt DateTime? @map("published_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") clip Clip @relation(fields: [clipId], references: [id], onDelete: Cascade) @@index([clipId, platform]) @@index([status]) @@map("export_jobs") } model PerformanceMetric { id String @id @default(uuid()) @db.Uuid clipId String @map("clip_id") @db.Uuid viewCount Int @default(0) @map("view_count") clickThroughToEpisode Int @default(0) @map("click_through_to_episode") guestProfileVisits Int @default(0) @map("guest_profile_visits") engagementByPlatform Json @default("{}") @map("engagement_by_platform") lastSyncedAt DateTime? @map("last_synced_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") clip Clip @relation(fields: [clipId], references: [id], onDelete: Cascade) @@index([clipId]) @@map("performance_metrics") } // ═══════════════════════════════════════════════════════════ // ─── Verified Guest Network ────────────────────────────── // Two-sided marketplace: Guest profiles, reputation, discovery // ═══════════════════════════════════════════════════════════ enum VerificationBadgeType { linkedin email podcast_link identity website } enum VerificationStatus { basic verified featured } enum BookmarkStatus { saved contacted booked } enum ProfileViewSource { search suggestion direct } enum NetworkBookingRequestStatus { pending accepted declined expired } enum ReputationTier { bronze silver gold } // ─── Guest Public Profile (Network Identity) ───────── model GuestPublicProfile { id String @id @default(uuid()) @db.Uuid userId String @unique @map("user_id") @db.Uuid isPublic Boolean @default(false) @map("is_public") vanityUrl String @unique @map("vanity_url") headline String? fullBio String? @map("full_bio") @db.Text country String? city String? languages String[] @default([]) timezone String? responseTimeHours Int? @map("response_time_hours") verificationStatus VerificationStatus @default(basic) @map("verification_status") reliabilityScore Decimal @default(0) @map("reliability_score") @db.Decimal(5, 2) totalAppearances Int @default(0) @map("total_appearances") averageRating Decimal @default(0) @map("average_rating") @db.Decimal(3, 2) socialProof Json @default("{}") @map("social_proof") visibilitySettings Json @default("{}") @map("visibility_settings") availableFrom DateTime? @map("available_from") phoneNumber String? @map("phone_number") whatsappNumber String? @map("whatsapp_number") avatarUrl String? @map("avatar_url") tier ReputationTier @default(bronze) verificationBadges VerificationBadgeType[] @default([]) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) expertiseTags ExpertiseTag[] availabilityWindows AvailabilityWindow[] bookingHistory BookingHistory[] reliabilityMetrics ReliabilityMetric[] bookmarks NetworkBookmark[] profileViews ProfileView[] bookingRequests NetworkBookingRequest[] hostReviews HostReview[] trustScore TrustScore? @@index([country]) @@index([verificationStatus]) @@index([reliabilityScore]) @@index([tier]) @@index([isPublic, verificationStatus]) @@map("guest_public_profiles") } // ─── Trust Score (Composite Trust Metric) ──────────── model TrustScore { id String @id @default(uuid()) @db.Uuid profileId String @unique @map("profile_id") @db.Uuid score Float @default(35.0) // 0.0 – 100.0 ratingNorm Float @default(0) @map("rating_norm") // R_norm component verification Float @default(0.3) // V component activity Float @default(0) @map("activity") // A component mlReliability Float @default(0.5) @map("ml_reliability") // M component (placeholder) components Json @default("{}") // full breakdown JSON lastUpdated DateTime @default(now()) @map("last_updated") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") profile GuestPublicProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) @@index([score]) @@map("trust_scores") } // ─── Expertise Tags ────────────────────────────────── model ExpertiseTag { id String @id @default(uuid()) @db.Uuid profileId String @map("profile_id") @db.Uuid name String category String? profile GuestPublicProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) @@unique([profileId, name]) @@index([name]) @@map("expertise_tags") } // ─── Availability Windows ──────────────────────────── model AvailabilityWindow { id String @id @default(uuid()) @db.Uuid profileId String @map("profile_id") @db.Uuid dayOfWeek Int @map("day_of_week") // 0=Sunday, 6=Saturday startHour Int @map("start_hour") // 0-23 endHour Int @map("end_hour") // 0-23 timezone String @default("UTC") profile GuestPublicProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) @@index([profileId]) @@map("availability_windows") } // ─── Booking History (Cross-Show Track Record) ─────── model BookingHistory { id String @id @default(uuid()) @db.Uuid profileId String @map("profile_id") @db.Uuid episodeId String? @map("episode_id") @db.Uuid podcastName String @map("podcast_name") hostName String? @map("host_name") appearedAt DateTime @map("appeared_at") isPublic Boolean @default(true) @map("is_public") topicsDiscussed Json @default("[]") @map("topics_discussed") episodePerformanceTier String? @map("episode_performance_tier") createdAt DateTime @default(now()) @map("created_at") profile GuestPublicProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) episode Episode? @relation(fields: [episodeId], references: [id], onDelete: SetNull) @@index([profileId]) @@index([episodeId]) @@map("booking_history") } // ─── Reliability Metrics (Behavioral Scoring) ──────── model ReliabilityMetric { id String @id @default(uuid()) @db.Uuid profileId String @map("profile_id") @db.Uuid responseRate Float @default(0) @map("response_rate") assetDeliveryRate Float @default(0) @map("asset_delivery_rate") showUpRate Float @default(0) @map("show_up_rate") engagementScore Float @default(0) @map("engagement_score") totalBookings Int @default(0) @map("total_bookings") calculatedAt DateTime @default(now()) @map("calculated_at") profile GuestPublicProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) @@index([profileId]) @@map("reliability_metrics") } // ─── Network Bookmarks (Host Saves) ────────────────── model NetworkBookmark { id String @id @default(uuid()) @db.Uuid podcastId String @map("podcast_id") @db.Uuid guestProfileId String @map("guest_profile_id") @db.Uuid status BookmarkStatus @default(saved) notes String? @db.Text createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") podcast Podcast @relation(fields: [podcastId], references: [id], onDelete: Cascade) guestProfile GuestPublicProfile @relation(fields: [guestProfileId], references: [id], onDelete: Cascade) @@unique([podcastId, guestProfileId]) @@map("network_bookmarks") } // ─── Profile Views (Analytics) ─────────────────────── model ProfileView { id String @id @default(uuid()) @db.Uuid profileId String @map("profile_id") @db.Uuid viewerPodcastId String? @map("viewer_podcast_id") @db.Uuid viewedAt DateTime @default(now()) @map("viewed_at") source ProfileViewSource @default(direct) profile GuestPublicProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) viewerPodcast Podcast? @relation("ViewerPodcast", fields: [viewerPodcastId], references: [id], onDelete: SetNull) @@index([profileId, viewedAt]) @@index([viewerPodcastId]) @@map("profile_views") } // ─── Graph Edges (Social Graph Persistence) ─────────── model GraphEdge { id String @id @default(uuid()) @db.Uuid source String target String type String // "appeared_on" | "hosted" | "booked" | "knows" | "similar_to" weight Float @default(1.0) metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@unique([source, target, type]) @@index([source]) @@index([target]) @@index([source, type]) @@map("graph_edges") } // ─── Network Booking Requests ──────────────────────── model NetworkBookingRequest { id String @id @default(uuid()) @db.Uuid fromPodcastId String @map("from_podcast_id") @db.Uuid toProfileId String @map("to_profile_id") @db.Uuid status NetworkBookingRequestStatus @default(pending) message String @db.Text proposedTopics Json @default("[]") @map("proposed_topics") proposedDates Json @default("[]") @map("proposed_dates") respondedAt DateTime? @map("responded_at") notifiedAt DateTime? @map("notified_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") fromPodcast Podcast @relation(fields: [fromPodcastId], references: [id], onDelete: Cascade) toProfile GuestPublicProfile @relation(fields: [toProfileId], references: [id], onDelete: Cascade) review HostReview? @@index([toProfileId, status]) @@index([fromPodcastId]) @@map("network_booking_requests") } // ─── Host Reviews (Post-Episode Feedback) ──────────── model HostReview { id String @id @default(uuid()) @db.Uuid bookingRequestId String @unique @map("booking_request_id") @db.Uuid profileId String @map("profile_id") @db.Uuid contentRating Int @map("content_rating") // 1-5 prepQuality Int @map("prep_quality") // 1-5 overallRating Int @map("overall_rating") // 1-5 comment String? @db.Text createdAt DateTime @default(now()) @map("created_at") bookingRequest NetworkBookingRequest @relation(fields: [bookingRequestId], references: [id], onDelete: Cascade) profile GuestPublicProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) @@index([profileId]) @@map("host_reviews") } // ─── Subscription Tiers & Platform-Managed Credentials ─── enum SubscriptionPlanId { hobby creator studio } enum BillingProvider { stripe mpesa paystack } enum SubscriptionStatus { active pending trialing past_due canceled incomplete } // ─── Platform Admin Roles (RBAC) ──────────────────────── // // Granular admin roles. Each role carries a baseline permission set defined // in packages/shared-types/src/admin.ts (ADMIN_ROLE_PERMISSIONS). Per-user // permission overrides live on User.adminPermissions (JSON). // // SUPER_ADMIN → every permission (god-mode, break-glass) // OPS → queues, jobs, health, workspace ops, billing read // SUPPORT → users, workspaces, moderation resolve, support tickets // MODERATOR → moderation read/resolve/takedown, user & workspace notes // BILLING → billing read/refund/plan override, credits // ANALYST → read-only across users, workspaces, billing, audit export // READ_ONLY → read-only across users, workspaces, billing, audit enum AdminRole { super_admin ops support moderator billing analyst read_only } // ─── Workspace Verification (Anti-Squatting) ──────────── // // Lifecycle of a workspace's identity claim on its subdomain: // // UNVERIFIED ──► PENDING_REVIEW ──► CLAIMED ──► VERIFIED ──► FEATURED // │ │ │ │ // ▼ ▼ ▼ ▼ // blocked manual auto via admin // public review artifacts curated // listing queue // // Anti-squatting rationale: // • UNVERIFIED workspaces can be created by anyone, but cannot appear in // public directories, network search, or "featured" placements. This // lets the real owner discover the squat and file a claim. // • Any workspace whose slug matches an entry in `BrandProtection` // starts at PENDING_REVIEW and cannot self-promote to VERIFIED — only // the legitimate owner can claim it via DNS / RSS / identity checks. // • Slugs that get flagged with `trademark_indicator` / // `bulk_creation` heuristics also require manual review. // • See docs/workspace-verification.md for the full ruleset. enum WorkspaceVerificationStatus { unverified pending_review claimed verified featured } // Why a workspace was flagged during eligibility evaluation. Stored as // an array on WorkspaceVerification.autoFlags so admins can see the // full context at a glance. enum WorkspaceVerificationReason { brand_match // slug exactly matches BrandProtection entry short_slug // slug ≤ 3 chars (high value, prone to hoarding) generic_word // slug is a single common English word (music, tech, news) trademark_indicator // slug contains "official", "real", "tm", "®", "©", etc. bulk_creation // user created 5+ workspaces in the last 24h suspicious_keywords // other heuristic matches (e.g. impersonation of known brand prefix) manual_flag // admin manually flagged reserved_namespace // matches RESERVED_SUBDOMAINS redemption_window // slug is in the previous-owner's redemption window } enum BrandProtectionCategory { podcast media_company tech_company government ngo public_figure other } enum SlugRedemptionReason { renamed deleted expired_inactive verification_rejected reclaimed_by_admin } // Which integrations each subscription tier unlocks. // The platform (you, the developer) sets the credentials once; // workspaces inherit access based on their subscription level. model SubscriptionTierIntegrationAccess { id String @id @default(uuid()) @db.Uuid planId SubscriptionPlanId @map("plan_id") provider IntegrationProvider isAvailable Boolean @default(true) @map("is_available") credentialSource String @default("platform") @map("credential_source") // "platform" or "workspace" createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@unique([planId, provider]) @@index([planId]) @@map("subscription_tier_integration_access") } // Platform-managed credentials for third-party integrations. // Set once by the developer; workspaces on the matching tier // automatically use these credentials without ever seeing them. model PlatformIntegrationCredential { id String @id @default(uuid()) @db.Uuid provider IntegrationProvider @unique accessToken String? @map("access_token") refreshToken String? @map("refresh_token") tokenExpiresAt DateTime? @map("token_expires_at") apiKey String? @map("api_key") apiSecret String? @map("api_secret") endpointUrl String? @map("endpoint_url") scopes String[] @default([]) metadata Json @default("{}") isActive Boolean @default(true) @map("is_active") lastValidatedAt DateTime? @map("last_validated_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([provider, isActive]) @@map("platform_integration_credentials") } // ─── Third-Party Integrations ───────────────────── enum IntegrationProvider { google_calendar google_mail outlook riverside zoom cloudflare_r2 deepgram nvidia_nim deepseek moonshot // Self-hosted open-source integration providers calcom chatwoot documenso formbricks listmonk pretix } enum IntegrationStatus { connected disconnected expired error } model WorkspaceIntegration { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid provider IntegrationProvider status IntegrationStatus @default(disconnected) accessToken String? @map("access_token") refreshToken String? @map("refresh_token") tokenExpiresAt DateTime? @map("token_expires_at") scopes String[] @default([]) providerAccountId String? @map("provider_account_id") providerEmail String? @map("provider_email") providerName String? @map("provider_name") metadata Json @default("{}") lastSyncAt DateTime? @map("last_sync_at") errorMessage String? @map("error_message") @db.Text createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) events IntegrationEvent[] @@unique([workspaceId, provider]) @@index([provider, status]) @@map("workspace_integrations") } // ─── Integration Events (Synced Calendar Events, etc.) ─ model IntegrationEvent { id String @id @default(uuid()) @db.Uuid integrationId String @map("integration_id") @db.Uuid providerEventId String @map("provider_event_id") title String description String? @db.Text startTime DateTime @map("start_time") endTime DateTime @map("end_time") timezone String @default("UTC") location String? meetLink String? @map("meet_link") attendees Json @default("[]") rawData Json @default("{}") @map("raw_data") isSynced Boolean @default(true) @map("is_synced") lastSyncedAt DateTime? @map("last_synced_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") integration WorkspaceIntegration @relation(fields: [integrationId], references: [id], onDelete: Cascade) @@unique([integrationId, providerEventId]) @@index([startTime, endTime]) @@index([integrationId, isSynced]) @@map("integration_events") } // ─── Creator Sponsorship Invoices ──────────────────── model SponsorInvoice { id String @id @default(uuid()) @db.Uuid podcastId String @map("podcast_id") @db.Uuid sponsorName String @map("sponsor_name") sponsorPhone String @map("sponsor_phone") amountKes Int @map("amount_kes") platformCutKes Int @map("platform_cut_kes") payoutAmountKes Int @map("payout_amount_kes") status String @default("pending") // "pending", "paid", "disbursed", "failed" payheroReference String? @map("payhero_reference") payoutReference String? @map("payout_reference") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") podcast Podcast @relation(fields: [podcastId], references: [id], onDelete: Cascade) @@index([podcastId]) @@map("sponsor_invoices") } // ─── Profile Settings ─────────────────────────── model UserProfileSettings { id String @id @default(uuid()) @db.Uuid userId String @unique @map("user_id") @db.Uuid bio String? @db.Text designation String? location String? avatarUrl String? @map("avatar_url") phoneNumber String? @map("phone_number") timezone String @default("UTC") dateFormat String @default("MM/DD/YYYY") @map("date_format") timeFormat String @default("12h") @map("time_format") locale String @default("en-US") weekStartsOn Int @default(0) @map("week_starts_on") // 0=Sunday, 1=Monday socialLinks Json @default("{}") @map("social_links") metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_profile_settings") } // ─── Notification Preferences ────────────────── enum PushProvider { expo firebase apns } // ─── Service Rating Types ───────────────────────── // The type of platform service a user can rate. Used by ServiceRating to // capture satisfaction across different areas of the product. enum RatingServiceType { PLATFORM BOOKING EVENT SUPPORT TICKETING } // ─── Survey Question Types ──────────────────────── // The type of a survey question, which determines how the answer is captured // and validated. enum SurveyQuestionType { TEXT TEXTAREA RADIO CHECKBOX SELECT RATING } model UserNotificationPreferences { id String @id @default(uuid()) @db.Uuid userId String @unique @map("user_id") @db.Uuid pushEnabled Boolean @default(true) @map("push_enabled") emailEnabled Boolean @default(true) @map("email_enabled") smsEnabled Boolean @default(false) @map("sms_enabled") bookingReminders Boolean @default(true) @map("booking_reminders") assetReminders Boolean @default(true) @map("asset_reminders") outreachReplies Boolean @default(true) @map("outreach_replies") networkRequests Boolean @default(true) @map("network_requests") recordingAlerts Boolean @default(true) @map("recording_alerts") billingAlerts Boolean @default(true) @map("billing_alerts") marketingEmails Boolean @default(false) @map("marketing_emails") digestFrequency String @default("daily") @map("digest_frequency") // daily, weekly, never quietHoursStart String? @map("quiet_hours_start") // HH:mm quietHoursEnd String? @map("quiet_hours_end") // HH:mm pushProvider PushProvider? @map("push_provider") pushToken String? @map("push_token") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_notification_preferences") } // ─── Web Push Subscriptions (Browser Push) ─────── // // Browser push subscriptions stored per user. A user may have multiple // subscriptions (one per browser/device). Used by the notification // processor to deliver web push via the `web-push` library with VAPID keys. model WebPushSubscription { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid endpoint String @unique p256dhKey String @map("p256dh_key") authKey String @map("auth_key") deviceInfo String? @map("device_info") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@map("web_push_subscriptions") } // ─── Service Ratings ────────────────────────────── // // A user's satisfaction rating for a platform service (1–5, optional comment). // One rating per user per (serviceType, serviceId) pair. This is the native // source of truth for user satisfaction, independent of third-party tools. model ServiceRating { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid serviceType RatingServiceType @map("service_type") serviceId String @map("service_id") rating Int // 1–5 comment String? @db.Text createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([userId, serviceType, serviceId], map: "service_rating_user_service_key") @@index([serviceType, serviceId]) @@map("service_ratings") } // ─── Surveys ────────────────────────────────────── // // Native survey system owned by the platform. Surveys are created and managed // in the admin panel; users submit responses via the web/mobile apps. model Survey { id String @id @default(uuid()) @db.Uuid title String description String? isActive Boolean @default(true) @map("is_active") createdBy String @map("created_by") @db.Uuid expiresAt DateTime? @map("expires_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") creator User @relation("SurveyCreator", fields: [createdBy], references: [id], onDelete: SetNull) questions SurveyQuestion[] responses SurveyResponse[] @@index([isActive]) @@index([createdBy]) @@map("surveys") } model SurveyQuestion { id String @id @default(uuid()) @db.Uuid surveyId String @map("survey_id") @db.Uuid questionText String @map("question_text") questionType SurveyQuestionType @map("question_type") options Json? // For RADIO/CHECKBOX/SELECT: ["opt1", "opt2", ...] isRequired Boolean @default(true) @map("is_required") order Int @default(0) createdAt DateTime @default(now()) @map("created_at") survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade) answers SurveyAnswer[] @@index([surveyId]) @@map("survey_questions") } model SurveyResponse { id String @id @default(uuid()) @db.Uuid surveyId String @map("survey_id") @db.Uuid userId String @map("user_id") @db.Uuid submittedAt DateTime @default(now()) @map("submitted_at") survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) answers SurveyAnswer[] @@index([surveyId]) @@index([userId]) @@unique([surveyId, userId], map: "survey_response_survey_user_key") @@map("survey_responses") } model SurveyAnswer { id String @id @default(uuid()) @db.Uuid responseId String @map("response_id") @db.Uuid questionId String @map("question_id") @db.Uuid answerText String @db.Text @map("answer_text") response SurveyResponse @relation(fields: [responseId], references: [id], onDelete: Cascade) question SurveyQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) @@index([responseId]) @@index([questionId]) @@map("survey_answers") } // ─── Security Settings ───────────────────────── model UserSecuritySettings { id String @id @default(uuid()) @db.Uuid userId String @unique @map("user_id") @db.Uuid twoFactorEnabled Boolean @default(false) @map("two_factor_enabled") twoFactorMethod String? @map("two_factor_method") // authenticator, sms, email sessionTimeoutMinutes Int @default(480) @map("session_timeout_minutes") // 8 hours default apiKeyEnabled Boolean @default(false) @map("api_key_enabled") apiKeyLastUsed DateTime? @map("api_key_last_used") loginNotifications Boolean @default(true) @map("login_notifications") trustedDevices Json @default("[]") @map("trusted_devices") allowedIpRanges Json @default("[]") @map("allowed_ip_ranges") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_security_settings") } // ─── Branding Settings (Workspace-level) ─────── model WorkspaceBrandingSettings { id String @id @default(uuid()) @db.Uuid workspaceId String @unique @map("workspace_id") @db.Uuid primaryColor String @default("#FF6B35") @map("primary_color") secondaryColor String @default("#6C5CE7") @map("secondary_color") accentColor String @default("#00B894") @map("accent_color") logoUrl String? @map("logo_url") faviconUrl String? @map("favicon_url") coverImageUrl String? @map("cover_image_url") fontFamily String @default("Inter") @map("font_family") customCss String? @map("custom_css") @db.Text customDomain String? @map("custom_domain") portalTitle String? @map("portal_title") portalDescription String? @map("portal_description") @db.Text metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@map("workspace_branding_settings") } // ─── Workspace General Settings ──────────────── model WorkspaceSettings { id String @id @default(uuid()) @db.Uuid workspaceId String @unique @map("workspace_id") @db.Uuid defaultLocale String @default("en-US") @map("default_locale") defaultTimezone String @default("UTC") @map("default_timezone") defaultDateFormat String @default("MM/DD/YYYY") @map("default_date_format") defaultTimeFormat String @default("12h") @map("default_time_format") weekStartsOn Int @default(0) @map("week_starts_on") recordingBufferBefore Int @default(15) @map("recording_buffer_before") recordingBufferAfter Int @default(15) @map("recording_buffer_after") defaultRecordingPlatform String @default("zoom") @map("default_recording_platform") autoConfirmBookings Boolean @default(false) @map("auto_confirm_bookings") guestPortalEnabled Boolean @default(true) @map("guest_portal_enabled") publicNetworkProfile Boolean @default(false) @map("public_network_profile") maxGuestsPerEvent Int @default(50) @map("max_guests_per_event") retentionDays Int @default(365) @map("retention_days") metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@map("workspace_settings") } // ─── Customer Support & User Inquiries ─────────────── model UserInquiry { id String @id @default(uuid()) @db.Uuid name String email String subject String message String @db.Text status String @default("pending") // "pending", "in_progress", "resolved", "closed" notes String? @db.Text assignedTo String? @map("assigned_to") @db.Uuid createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("user_inquiries") } // ═══════════════════════════════════════════════════════════ // ─── Real-time Collaboration (mic-realtime) ───────────── // Conversation trees, episode-aware messages, presence, tasks, // media-timestamp comments. Designed to be the system-of-record // while the Erlang/OTP service owns live fanout. // ═══════════════════════════════════════════════════════════ enum ConversationType { general episode guest direct workspace } enum ConversationMemberRole { owner member guest observer } enum TaskStatus { todo in_progress in_review done cancelled } enum TaskPriority { low medium high urgent } enum MediaCommentStatus { open resolved } // A conversation is a bounded thread. Episodes get one automatically; // podcasts get a `general` one; guests get one for cross-podcast // discussion; direct messages for 1:1; workspace for the whole team. model Conversation { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid podcastId String? @map("podcast_id") @db.Uuid episodeId String? @map("episode_id") @db.Uuid guestId String? @map("guest_id") @db.Uuid type ConversationType @default(general) title String description String? @db.Text isArchived Boolean @default(false) @map("is_archived") metadata Json @default("{}") createdById String @map("created_by_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") lastMessageAt DateTime? @map("last_message_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) podcast Podcast? @relation(fields: [podcastId], references: [id], onDelete: Cascade) episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade) guest Guest? @relation(fields: [guestId], references: [id], onDelete: Cascade) members ConversationMember[] messages ConversationMessage[] tasks Task[] @@unique([workspaceId, type, podcastId, episodeId, guestId], name: "conversation_scope") @@index([workspaceId, type]) @@index([podcastId]) @@index([episodeId]) @@index([guestId]) @@index([lastMessageAt]) @@map("conversations") } model ConversationMember { id String @id @default(uuid()) @db.Uuid conversationId String @map("conversation_id") @db.Uuid userId String @map("user_id") @db.Uuid role ConversationMemberRole @default(member) lastReadSeq Int @default(0) @map("last_read_seq") isMuted Boolean @default(false) @map("is_muted") joinedAt DateTime @default(now()) @map("joined_at") conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([conversationId, userId]) @@index([userId]) @@map("conversation_members") } // Append-only ordered event log. The Erlang service fans out to // connected sockets and increments `seq` on every entry; `seq` is the // canonical ordering across the realtime boundary. model ConversationMessage { id String @id @default(uuid()) @db.Uuid conversationId String @map("conversation_id") @db.Uuid userId String? @map("user_id") @db.Uuid // null for system events seq Int @default(0) eventType String @default("message") @map("event_type") // message | system | reaction | read | typing | episode.* | task.* body String? @db.Text metadata Json @default("{}") replyToId String? @map("reply_to_id") @db.Uuid editedAt DateTime? @map("edited_at") createdAt DateTime @default(now()) @map("created_at") conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) replyTo ConversationMessage? @relation("Reply", fields: [replyToId], references: [id], onDelete: SetNull) replies ConversationMessage[] @relation("Reply") reactions MessageReaction[] tasks Task[] mediaComments MediaComment[] @@unique([conversationId, seq]) @@index([conversationId, createdAt]) @@index([userId]) @@map("conversation_messages") } model MessageReaction { id String @id @default(uuid()) @db.Uuid messageId String @map("message_id") @db.Uuid userId String @map("user_id") @db.Uuid emoji String createdAt DateTime @default(now()) @map("created_at") message ConversationMessage @relation(fields: [messageId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([messageId, userId, emoji]) @@index([messageId]) @@map("message_reactions") } // A comment pinned to an exact moment of an asset (audio / video / // transcript). Clicking the comment jumps the editor's cursor to // `mediaTimestampSeconds`. This is the "episode-aware message" feature. model MediaComment { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid episodeId String @map("episode_id") @db.Uuid assetId String @map("asset_id") @db.Uuid assetType String @default("audio") @map("asset_type") // audio | video | transcript userId String @map("user_id") @db.Uuid conversationMessageId String? @map("conversation_message_id") @db.Uuid mediaTimestampSeconds Decimal @default(0) @map("media_timestamp_seconds") @db.Decimal(12, 3) body String @db.Text status MediaCommentStatus @default(open) resolvedById String? @map("resolved_by_id") @db.Uuid resolvedAt DateTime? @map("resolved_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) resolvedBy User? @relation("MediaCommentResolver", fields: [resolvedById], references: [id], onDelete: SetNull) conversationMessage ConversationMessage? @relation(fields: [conversationMessageId], references: [id], onDelete: SetNull) @@index([episodeId, assetId, mediaTimestampSeconds]) @@index([workspaceId]) @@index([conversationMessageId]) @@index([status]) @@map("media_comments") } // Tasks turn chat messages into operational work. They live in a // conversation, belong to a workspace, and may be linked to an episode. model Task { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid conversationId String? @map("conversation_id") @db.Uuid podcastId String? @map("podcast_id") @db.Uuid episodeId String? @map("episode_id") @db.Uuid title String description String? @db.Text status TaskStatus @default(todo) priority TaskPriority @default(medium) assigneeId String? @map("assignee_id") @db.Uuid createdById String @map("created_by_id") @db.Uuid sourceMessageId String? @map("source_message_id") @db.Uuid mediaTimestampSeconds Decimal? @map("media_timestamp_seconds") @db.Decimal(12, 3) dueAt DateTime? @map("due_at") completedAt DateTime? @map("completed_at") metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) conversation Conversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull) podcast Podcast? @relation(fields: [podcastId], references: [id], onDelete: Cascade) episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade) assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id], onDelete: SetNull) createdBy User @relation("TaskCreator", fields: [createdById], references: [id], onDelete: Cascade) sourceMessage ConversationMessage? @relation(fields: [sourceMessageId], references: [id], onDelete: SetNull) @@index([workspaceId, status]) @@index([assigneeId, status]) @@index([episodeId]) @@index([conversationId]) @@index([dueAt]) @@map("tasks") } // ─── Ticketing (Pretix) ───────────────────────────────── // Self-hosted ticketing for events. Pretix is the backend; // these models track events, tickets, orders, and order items // (with QR codes) in our own DB for fast reads and the mobile UI. enum TicketStatus { draft published archived } enum OrderStatus { pending paid cancelled refunded voided } model Event { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid title String description String? startDate DateTime @map("start_date") endDate DateTime @map("end_date") timezone String @default("UTC") status TicketStatus @default(draft) currency String @default("USD") pretixEventId String? @unique @map("pretix_event_id") pretixOrganizerId String? @map("pretix_organizer_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) tickets Ticket[] orders Order[] coupons Coupon[] @@index([workspaceId]) @@index([pretixEventId]) @@map("events") } model Ticket { id String @id @default(uuid()) @db.Uuid eventId String @map("event_id") @db.Uuid name String type String // e.g. "early_bird", "vip", "standard" price Decimal @db.Decimal(10, 2) currency String @default("USD") quantity Int isActive Boolean @default(true) @map("is_active") pretixItemId String? @unique @map("pretix_item_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) orderItems OrderItem[] questions TicketQuestion[] @@index([eventId]) @@map("tickets") } model Order { id String @id @default(uuid()) @db.Uuid eventId String @map("event_id") @db.Uuid userId String? @map("user_id") @db.Uuid guestEmail String? @map("guest_email") status OrderStatus @default(pending) total Decimal @db.Decimal(10, 2) currency String @default("USD") pretixOrderId String? @unique @map("pretix_order_id") couponId String? @map("coupon_id") @db.Uuid discountAmountCents Int? @map("discount_amount_cents") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) coupon Coupon? @relation(fields: [couponId], references: [id], onDelete: SetNull) items OrderItem[] @@index([eventId]) @@index([userId]) @@index([pretixOrderId]) @@index([couponId]) @@map("orders") } model OrderItem { id String @id @default(uuid()) @db.Uuid orderId String @map("order_id") @db.Uuid ticketId String @map("ticket_id") @db.Uuid quantity Int @default(1) attendeeName String @map("attendee_name") attendeeEmail String @map("attendee_email") qrCode String @unique @map("qr_code") pretixOrderPositionId String? @unique @map("pretix_order_position_id") createdAt DateTime @default(now()) @map("created_at") order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) answers OrderItemAnswer[] checkIns CheckIn[] @@index([orderId]) @@index([ticketId]) @@index([qrCode]) @@map("order_items") } model CheckIn { id String @id @default(uuid()) @db.Uuid orderItemId String @map("order_item_id") @db.Uuid checkedInAt DateTime @default(now()) @map("checked_in_at") checkedInBy String? @map("checked_in_by") method String @default("qr") @map("method") // qr | manual | nfc eventId String @map("event_id") @db.Uuid orderItem OrderItem @relation(fields: [orderItemId], references: [id], onDelete: Cascade) @@unique([orderItemId], name: "check_in_order_item_id_key") @@index([orderItemId]) @@index([eventId]) @@map("check_ins") } // ════════════════════════════════════════════════════════════════════════════════════ // ─── In-App Wallet (Revenue Collection for Ticketing) ────────────────────────── // Each workspace has one wallet. Ticket sales credit the organizer's wallet // (minus the platform cut). Wallet transactions form an immutable audit trail. // ════════════════════════════════════════════════════════════════════════════════════ enum WalletTransactionType { credit debit platform_fee payout refund } enum WalletTransactionStatus { pending completed failed reversed } model Wallet { id String @id @default(uuid()) @db.Uuid workspaceId String @unique @map("workspace_id") @db.Uuid balance Decimal @default(0) @db.Decimal(12, 2) @map("balance") currency String @default("USD") @map("currency") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) transactions WalletTransaction[] payouts Payout[] @@index([workspaceId]) @@map("wallets") } model WalletTransaction { id String @id @default(uuid()) @db.Uuid walletId String @map("wallet_id") @db.Uuid type WalletTransactionType @map("type") status WalletTransactionStatus @default(pending) @map("status") amount Decimal @db.Decimal(12, 2) @map("amount") currency String @default("USD") @map("currency") platformCut Decimal? @db.Decimal(12, 2) @map("platform_cut") // platform fee for credit txns netAmount Decimal? @db.Decimal(12, 2) @map("net_amount") // amount credited to wallet after platform cut reference String @map("reference") // e.g. orderId, payoutId referenceType String? @map("reference_type") // "order", "payout", "refund", "topup" description String? @db.Text @map("description") createdAt DateTime @default(now()) @map("created_at") wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) @@index([walletId]) @@index([status]) @@index([reference]) @@index([createdAt]) @@map("wallet_transactions") } // ─── Support / Customer Service (Chatwoot Integration) ──────────────── // Native support ticketing system that maps to Chatwoot conversations. // Provides SLA tracking, agent assignment, CSAT ratings, canned responses, // and bidirectional sync with a self-hosted Chatwoot instance. enum SupportCategory { general billing technical feature_request bug_report account } enum SupportPriority { low medium high urgent } enum SupportStatus { open pending resolved closed } enum SupportMessageType { inbound outbound system } // A support ticket — optionally linked to a Chatwoot conversation via // `chatwootConversationId`. One ticket per user per conversation scope. model SupportTicket { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid userId String? @map("user_id") @db.Uuid guestId String? @map("guest_id") @db.Uuid chatwootConversationId Int? @unique @map("chatwoot_conversation_id") chatwootInboxId Int? @map("chatwoot_inbox_id") chatwootContactId Int? @map("chatwoot_contact_id") category SupportCategory @default(general) priority SupportPriority @default(medium) status SupportStatus @default(open) subject String description String? @db.Text assigneeId String? @map("assignee_id") @db.Uuid resolvedAt DateTime? @map("resolved_at") firstResponseAt DateTime? @map("first_response_at") slaDueAt DateTime? @map("sla_due_at") slaBreached Boolean @default(false) metadata Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) user User? @relation("SupportTicketUser", fields: [userId], references: [id], onDelete: SetNull) guest Guest? @relation("SupportTicketGuest", fields: [guestId], references: [id], onDelete: SetNull) assignee User? @relation("SupportAssignee", fields: [assigneeId], references: [id], onDelete: SetNull) messages SupportMessage[] ratings SupportSatisfactionRating[] agentAssignments SupportAgentAssignment[] @relation("SupportTicketAssignments") @@index([workspaceId, status]) @@index([assigneeId]) @@index([chatwootConversationId]) @@index([status, priority]) @@map("support_tickets") } model SupportMessage { id String @id @default(uuid()) @db.Uuid ticketId String @map("ticket_id") @db.Uuid userId String? @map("user_id") @db.Uuid chatwootMessageId Int? @unique @map("chatwoot_message_id") body String @db.Text messageType SupportMessageType @default(inbound) createdAt DateTime @default(now()) @map("created_at") ticket SupportTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) @@index([ticketId, createdAt]) @@map("support_messages") } model SupportAgentAssignment { id String @id @default(uuid()) @db.Uuid ticketId String @map("ticket_id") @db.Uuid agentId String @map("agent_id") @db.Uuid assignedAt DateTime @default(now()) @map("assigned_at") unassignedAt DateTime? @map("unassigned_at") ticket SupportTicket @relation("SupportTicketAssignments", fields: [ticketId], references: [id], onDelete: Cascade) agent User @relation(fields: [agentId], references: [id], onDelete: Cascade) @@index([ticketId]) @@index([agentId]) @@map("support_agent_assignments") } model SupportCategoryConfig { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid name String color String? @default("#6B7280") isActive Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@index([workspaceId]) @@map("support_categories") } model SupportSLA { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid planId String firstResponseMinutes Int @default(60) @map("first_response_minutes") resolutionHours Int @default(24) @map("resolution_hours") isActive Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@index([workspaceId, planId]) @@map("support_slas") } model CannedResponse { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid title String content String @db.Text category String? usageCount Int @default(0) @map("usage_count") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@index([workspaceId]) @@map("canned_responses") } model SupportSatisfactionRating { id String @id @default(uuid()) @db.Uuid ticketId String @map("ticket_id") @db.Uuid userId String @map("user_id") @db.Uuid rating Int // 1–5 comment String? @db.Text createdAt DateTime @default(now()) @map("created_at") ticket SupportTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([ticketId, userId]) @@index([ticketId]) @@index([userId]) @@map("support_satisfaction_ratings") } // ─── Outbound Webhooks ────────────────────────────────────────── // // User-registered webhook endpoints that receive POST payloads when // platform events occur (guest status changes, booking confirmations, // recording completed, etc.). Each delivery is logged with HTTP status // and response body for observability and retry analysis. enum WebhookDeliveryStatus { pending sent failed retrying } model WebhookEndpoint { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid url String @db.Text secret String @map("secret") events String[] @default([]) @map("events") isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) deliveries WebhookDelivery[] @@index([workspaceId]) @@index([isActive]) @@map("webhook_endpoints") } model WebhookDelivery { id String @id @default(uuid()) @db.Uuid endpointId String @map("endpoint_id") @db.Uuid event String @map("event") payload Json @map("payload") responseStatus Int? @map("response_status") responseBody String? @db.Text @map("response_body") status WebhookDeliveryStatus @default(pending) @map("status") attemptedAt DateTime? @map("attempted_at") createdAt DateTime @default(now()) @map("created_at") endpoint WebhookEndpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade) @@index([endpointId]) @@index([status]) @@index([createdAt]) @@map("webhook_deliveries") } // ─── Platform Governance ─────────────────────────────────────────────── // // Configuration and oversight models for platform administration. These // underpin the admin console: feature flags, platform settings, announcements, // abuse reports, admin notes, and scheduled supervision sweeps. // // Every mutation on these models should be paired with an AuditLog entry // (actor, action, before/after diff) so actions remain auditable for // law-enforcement and compliance purposes. enum PlatformSettingCategory { general security billing moderation operations } enum AnnouncementAudienceScope { all workspace user } enum AbuseReportStatus { open reviewing resolved dismissed } enum AbuseReportCategory { spam harassment impersonation intellectual_property illegal_content terms_violation other } enum SupervisionRunStatus { running completed failed } enum SupervisionFindingStatus { open acknowledged resolved dismissed } enum SupervisionFindingSeverity { low medium high critical } enum SupervisionFindingKind { stale_subscription failed_webhook sla_breach orphaned_record queue_failure suspicious_activity other } // ─── Platform Setting ───────────────────────────────────────────────── // // Key-value platform configuration (e.g. maintenance mode, signup gate, // default plan limits). Admins upsert via admin.settings.* procedures. model PlatformSetting { key String @id @map("key") value Json @map("value") description String? @map("description") category PlatformSettingCategory @default(general) @map("category") updatedById String? @map("updated_by_id") @db.Uuid updatedAt DateTime @updatedAt @map("updated_at") updatedBy User? @relation(fields: [updatedById], references: [id]) @@index([category]) @@map("platform_settings") } // ─── Feature Flag ───────────────────────────────────────────────────── // // Toggle platform features on/off, with optional rollout percentage and // workspace allowlist for gradual rollout or kill-switching. model FeatureFlag { key String @id @map("key") enabled Boolean @default(false) @map("enabled") rolloutPercentage Int @default(0) @map("rollout_percentage") // 0-100 allowlistWorkspaceIds String[] @default([]) @map("allowlist_workspace_ids") description String? @map("description") updatedById String? @map("updated_by_id") @db.Uuid updatedAt DateTime @updatedAt @map("updated_at") updatedBy User? @relation(fields: [updatedById], references: [id]) @@map("feature_flags") } // ─── Abuse Report ───────────────────────────────────────────────────── // // Reports of abuse, spam, or illegal content targeting users, workspaces, // or content. Reviewed by moderation admins. model AbuseReport { id String @id @default(uuid()) @db.Uuid reporterUserId String? @map("reporter_user_id") @db.Uuid targetType String @map("target_type") // user | workspace | episode | etc. targetId String @map("target_id") @db.Uuid category AbuseReportCategory @default(other) @map("category") details Json @default("{}") @map("details") status AbuseReportStatus @default(open) @map("status") resolution String? @db.Text @map("resolution") handledById String? @map("handled_by_id") @db.Uuid resolvedAt DateTime? @map("resolved_at") createdAt DateTime @default(now()) @map("created_at") reporter User? @relation("AbuseReportReporter", fields: [reporterUserId], references: [id]) handledBy User? @relation("AbuseReportHandler", fields: [handledById], references: [id]) @@index([status]) @@index([category]) @@index([targetType, targetId]) @@index([createdAt]) @@map("abuse_reports") } // ─── Admin Note ─────────────────────────────────────────────────────── // // Free-form notes admins attach to any entity (user, workspace, etc.) for // context during investigations. model AdminNote { id String @id @default(uuid()) @db.Uuid entityType String @map("entity_type") // user | workspace | episode | etc. entityId String @map("entity_id") @db.Uuid body String @db.Text @map("body") createdById String @map("created_by_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade) @@index([entityType, entityId]) @@index([createdById]) @@map("admin_notes") } // ─── Announcement ───────────────────────────────────────────────────── // // Platform-wide announcements broadcast to users/workspaces. Severity drives // display urgency (banner vs. toast vs. email). model Announcement { id String @id @default(uuid()) @db.Uuid title String @map("title") body String @db.Text @map("body") audience Json @default("{}") @map("audience") // { scope: "all"|"workspace"|"user", ids?: string[] } severity String @default("info") @map("severity") // info | warning | critical status String @default("published") @map("status") // draft | published | archived publishedAt DateTime? @map("published_at") expiresAt DateTime? @map("expires_at") createdById String @map("created_by_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade) @@index([status]) @@index([severity]) @@index([createdAt]) @@map("announcements") } // ─── Supervision Run ────────────────────────────────────────────────── // // Record of each scheduled or on-demand supervision sweep. Tracks what was // checked, when, and the outcome summary. model SupervisionRun { id String @id @default(uuid()) @db.Uuid kind SupervisionFindingKind @map("kind") status SupervisionRunStatus @default(running) @map("status") startedAt DateTime @default(now()) @map("started_at") finishedAt DateTime? @map("finished_at") summary Json @default("{}") @map("summary") triggeredById String? @map("triggered_by_id") @db.Uuid triggeredBy User? @relation(fields: [triggeredById], references: [id]) findings SupervisionFinding[] @@index([kind]) @@index([status]) @@index([startedAt]) @@map("supervision_runs") } // ─── Supervision Finding ────────────────────────────────────────────── // // An issue detected during a supervision sweep (e.g. past-due subscription, // failed webhook, SLA breach). Routed to super admins for review. model SupervisionFinding { id String @id @default(uuid()) @db.Uuid runId String @map("run_id") @db.Uuid kind SupervisionFindingKind @map("kind") severity SupervisionFindingSeverity @default(medium) @map("severity") title String @map("title") details Json @default("{}") @map("details") entityType String? @map("entity_type") entityId String? @map("entity_id") @db.Uuid status SupervisionFindingStatus @default(open) @map("status") assignedToId String? @map("assigned_to_id") @db.Uuid resolvedAt DateTime? @map("resolved_at") createdAt DateTime @default(now()) @map("created_at") run SupervisionRun @relation(fields: [runId], references: [id], onDelete: Cascade) assignedTo User? @relation(fields: [assignedToId], references: [id]) @@index([runId]) @@index([status]) @@index([severity]) @@index([kind]) @@index([entityType, entityId]) @@index([createdAt]) @@map("supervision_findings") } // ─── Coupons (Discount Codes) ───────────────────────────────────── // HustleSasa-competitive feature: coupon/discount codes applied at checkout. enum CouponType { PERCENTAGE FIXED_AMOUNT } model Coupon { id String @id @default(uuid()) @db.Uuid eventId String @map("event_id") @db.Uuid code String @unique @map("code") type CouponType @default(PERCENTAGE) @map("type") valueCents Int @map("value_cents") // percentage (e.g. 10 for 10%) or fixed amount in cents (e.g. 500 for $5.00) usageLimit Int? @map("usage_limit") usedCount Int @default(0) @map("used_count") validFrom DateTime? @map("valid_from") validUntil DateTime? @map("valid_until") isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) orders Order[] @@index([eventId]) @@index([code]) @@map("coupons") } // ─── Custom Attendee Questions ──────────────────────────────────── // HustleSasa-competitive feature: custom questions per ticket type // (text box, drop-down, confirmation checkbox, date picker). enum QuestionType { TEXT DROPDOWN CHECKBOX DATE } model TicketQuestion { id String @id @default(uuid()) @db.Uuid ticketId String @map("ticket_id") @db.Uuid question String type QuestionType @default(TEXT) @map("type") options Json? // For DROPDOWN: ["opt1", "opt2", ...] isRequired Boolean @default(true) @map("is_required") sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) answers OrderItemAnswer[] @@index([ticketId]) @@map("ticket_questions") } model OrderItemAnswer { id String @id @default(uuid()) @db.Uuid orderItemId String @map("order_item_id") @db.Uuid questionId String @map("question_id") @db.Uuid answerText String @map("answer_text") @db.Text createdAt DateTime @default(now()) @map("created_at") orderItem OrderItem @relation(fields: [orderItemId], references: [id], onDelete: Cascade) question TicketQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) @@index([orderItemId]) @@index([questionId]) @@map("order_item_answers") } // ─── Payouts / Withdrawals ──────────────────────────────────────── // HustleSasa-competitive feature: wallet payout/withdrawal requests. enum PayoutStatus { PENDING COMPLETED FAILED } enum PayoutMethod { BANK MOBILE_MONEY } model Payout { id String @id @default(uuid()) @db.Uuid walletId String @map("wallet_id") @db.Uuid amountCents Int @map("amount_cents") currency String @default("USD") status PayoutStatus @default(PENDING) @map("status") method PayoutMethod @default(BANK) @map("method") reference String @map("reference") createdAt DateTime @default(now()) @map("created_at") processedAt DateTime? @map("processed_at") wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) @@index([walletId]) @@index([status]) @@map("payouts") } // ════════════════════════════════════════════════════════════════════ // ─── Document Management, Sharing & Tracking ─────────────────────── // General-purpose document management: upload, share, track, verify. // Workspace-scoped and independent of the episode/assets domain. // ════════════════════════════════════════════════════════════════════ enum DocumentStatus { draft active revoked flagged quarantined } enum DocumentAction { view download share_create share_revoke upload delete } enum ShareType { public protected private } // ─── Document ─────────────────────────────────────────────────────── // Core document entity. One per uploaded file. SHA-256 hash enables // tamper detection and duplicate detection. model Document { id String @id @default(uuid()) @db.Uuid workspaceId String @map("workspace_id") @db.Uuid ownerId String @map("owner_id") @db.Uuid title String description String? @db.Text mimeType String @map("mime_type") sizeBytes Int @map("size_bytes") sha256 String // SHA-256 of current version content storageKey String @map("storage_key") // R2 object key status DocumentStatus @default(active) isForgeryFlagged Boolean @default(false) @map("is_forgery_flagged") isSanctionBlocked Boolean @default(false) @map("is_sanction_blocked") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) versions DocumentVersion[] shares DocumentShare[] accessLogs DocumentAccessLog[] securityFlags SecurityFlag[] @@index([workspaceId]) @@index([ownerId]) @@index([status]) @@index([sha256]) @@map("documents") } // ─── Document Version ─────────────────────────────────────────────── // Version history. Each upload creates a new version with its own hash. model DocumentVersion { id String @id @default(uuid()) @db.Uuid documentId String @map("document_id") @db.Uuid versionNo Int @map("version_no") // 1, 2, 3... mimeType String @map("mime_type") sizeBytes Int @map("size_bytes") sha256 String // SHA-256 of this version's content storageKey String @map("storage_key") uploadedAt DateTime @default(now()) @map("uploaded_at") uploadedById String? @map("uploaded_by_id") @db.Uuid document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) uploadedBy User? @relation("DocumentVersionUploader", fields: [uploadedById], references: [id], onDelete: SetNull) @@unique([documentId, versionNo]) @@index([documentId]) @@map("document_versions") } // ─── Document Share ───────────────────────────────────────────────── // Share links with expiration, access code, download limit, revocable. model DocumentShare { id String @id @default(uuid()) @db.Uuid documentId String @map("document_id") @db.Uuid workspaceId String @map("workspace_id") @db.Uuid shareType ShareType @default(public) @map("share_type") accessCode String? // required for protected shares expiresAt DateTime? @map("expires_at") maxDownloads Int? @map("max_downloads") downloadCount Int @default(0) @map("download_count") isRevoked Boolean @default(false) @map("is_revoked") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) accessLogs DocumentAccessLog[] @@index([documentId]) @@index([workspaceId]) @@index([isRevoked]) @@map("document_shares") } // ─── Document Access Log ──────────────────────────────────────────── // Append-only log of every view/download (actor, timestamp, IP, UA, action). model DocumentAccessLog { id String @id @default(uuid()) @db.Uuid documentId String @map("document_id") @db.Uuid shareId String? @map("share_id") @db.Uuid userId String? @map("user_id") @db.Uuid // null for anonymous share access workspaceId String @map("workspace_id") @db.Uuid action DocumentAction @map("action") ipAddress String? @map("ip_address") userAgent String? @map("user_agent") createdAt DateTime @default(now()) @map("created_at") document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) share DocumentShare? @relation(fields: [shareId], references: [id], onDelete: SetNull) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@index([documentId, createdAt]) @@index([workspaceId, createdAt]) @@index([action]) @@map("document_access_logs") } // ─── Security Blocklist ─────────────────────────────────────────────── // Email domains, IP ranges, and workspace slugs blocked at the platform // level for crime syndicate / forgery prevention. enum BlocklistType { EMAIL_DOMAIN IP_RANGE WORKSPACE_SLUG } model SecurityBlocklist { id String @id @default(uuid()) @db.Uuid type BlocklistType @map("type") value String @map("value") // email domain, IP/CIDR, or slug reason String @map("reason") isActive Boolean @default(true) @map("is_active") createdBy String? @map("created_by") @db.Uuid createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User? @relation(fields: [createdBy], references: [id], onDelete: SetNull) @@index([type, isActive]) @@index([value]) @@map("security_blocklists") } // ─── Security Flag ──────────────────────────────────────────────────── // Documents flagged by fraud/forgery detection or admin review. enum SecurityFlagType { FORGERY_SUSPECTED SANCTION_MATCH FRAUD_PATTERN BULK_UPLOAD RAPID_SHARING CROSS_WORKSPACE_SPIKE MANUAL_REVIEW } enum SecurityFlagStatus { OPEN REVIEWED DISMISSED ACTIONED } model SecurityFlag { id String @id @default(uuid()) @db.Uuid documentId String @map("document_id") @db.Uuid workspaceId String @map("workspace_id") @db.Uuid type SecurityFlagType @map("type") severity String @default("medium") @map("severity") // low, medium, high, critical reason String @map("reason") status SecurityFlagStatus @default(OPEN) @map("status") reviewedBy String? @map("reviewed_by") @db.Uuid reviewedAt DateTime? @map("reviewed_at") createdAt DateTime @default(now()) @map("created_at") document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) reviewer User? @relation(fields: [reviewedBy], references: [id], onDelete: SetNull) @@index([documentId]) @@index([workspaceId, status]) @@index([type, status]) @@map("security_flags") }
// ─── MicProtocol Prisma Schema ─────────────────────────
// Domain-driven schema for podcast guest management
generator client {
provider = "prisma-client-js"
previewFeatures = []
}
datasource db {
provider = "postgresql"
}
// ─── Enums ───────────────────────────────────────────
enum GuestStatus {
prospect
contacted
scheduled
recorded
published
}
enum GuestJourneyEventType {
created
contacted
scheduled
recorded
published
follow_up
milestone
note
}
enum EnergyPreference {
morning
afternoon
evening
flexible
}
enum EpisodeStatus {
draft
scheduled
recorded
editing
published
}
enum OutreachMessageType {
initial
followup
booking
reminder
}
enum OutreachSequenceStatus {
active
paused
completed
}
enum AssetRequestType {
headshot
bio
social_links
intro_audio
talking_points
}
enum AssetSubmissionStatus {
pending
approved
rejected
}
enum RecordingPlatform {
riverside
zoom
studio
micprotocol_native
}
enum DiagnosticStatus {
pending
passed
warning
failed
}
enum DiagnosticType {
rt60
emi_hum
storage
network_jitter
vad_mute
}
enum CodecType {
wav
opus