Spaces:
Running
Running
Download apps/api/prisma/schema.prisma from Benaah/MicProtocol: direct link, hf CLI and curl.
- Browser
- Download file 126 kB
-
https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/api/prisma/schema.prisma
- Command line
-
hf download hf://spaces/Benaah/MicProtocol/apps/api/prisma/schema.prisma
-
curl -L -o schema.prisma https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/api/prisma/schema.prisma
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