generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum AccountStatus { ACTIVE SUSPENDED DELETED } enum AuthProvider { PHONE APPLE WECHAT } enum RefreshTokenStatus { ACTIVE USED } enum ReviewStatus { DRAFT REVIEWING REJECTED MANUAL_REVIEW APPROVED } enum BottlePoolStatus { IN_POOL LEASED CONSUMED REMOVED CLOSED } enum BottlePickLeaseStatus { ACTIVE RETURNED EXPIRED CONSUMED } enum ConversationStatus { ACTIVE CLOSED } enum MessageStatus { SENT DELETED MODERATED } enum ReportStatus { PENDING REVIEWING RESOLVED DISMISSED } enum ModerationTaskStatus { PENDING PROCESSING COMPLETED } enum SanctionType { WARNING MUTE SUSPENSION BAN } enum NotificationStatus { PENDING SENT READ FAILED } enum OutboxStatus { PENDING PROCESSING PUBLISHED FAILED } model Account { id String @id @default(uuid()) @db.Uuid phoneCiphertext Bytes @map("phone_ciphertext") phoneHmac String @unique @map("phone_hmac") @db.VarChar(128) status AccountStatus @default(ACTIVE) tokenVersion Int @default(0) @map("token_version") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) anonymousProfile AnonymousProfile? authIdentities AuthIdentity[] sessions Session[] bottles Bottle[] pickLeases BottlePickLease[] @relation("PickerLeases") pickHistory BottlePickHistory[] @relation("PickerHistory") conversationMembers ConversationMember[] messages Message[] blocksInitiated Block[] @relation("Blocker") blocksReceived Block[] @relation("Blocked") reportsFiled Report[] @relation("Reporter") reportsAgainst Report[] @relation("Reported") moderationTasks ModerationTask[] @relation("Moderator") sanctionsReceived Sanction[] @relation("SanctionedAccount") sanctionsIssued Sanction[] @relation("SanctionIssuer") notifications Notification[] auditLogs AuditLog[] @relation("AuditActor") dailyUsage DailyUsage[] @@map("accounts") } model AnonymousProfile { id String @id @default(uuid()) @db.Uuid accountId String @unique @map("account_id") @db.Uuid publicId String @unique @default(uuid()) @map("public_id") @db.Uuid nickname String @db.VarChar(64) avatarKey String? @map("avatar_key") @db.VarChar(255) avatarColor String @map("avatar_color") @db.VarChar(16) bio String? @db.VarChar(500) reviewStatus ReviewStatus @default(REVIEWING) @map("review_status") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) @@map("anonymous_profiles") } model AuthIdentity { id String @id @default(uuid()) @db.Uuid accountId String @map("account_id") @db.Uuid provider AuthProvider providerSubject String @map("provider_subject") @db.VarChar(255) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) @@unique([provider, providerSubject]) @@index([accountId]) @@map("auth_identities") } model Session { id String @id @default(uuid()) @db.Uuid accountId String @map("account_id") @db.Uuid tokenVersion Int @default(0) @map("token_version") refreshTokenHash String @unique @map("refresh_token_hash") @db.VarChar(255) deviceId String? @map("device_id") @db.VarChar(255) expiresAt DateTime @map("expires_at") @db.Timestamptz(3) revokedAt DateTime? @map("revoked_at") @db.Timestamptz(3) tokenFamily String @map("token_family") @db.Uuid generation Int @default(0) previousTokenHash String? @unique @map("previous_token_hash") @db.VarChar(255) rotatedAt DateTime? @map("rotated_at") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) refreshTokens RefreshToken[] @@index([accountId, expiresAt]) @@index([tokenFamily]) @@map("sessions") } model RefreshToken { id String @id @default(uuid()) @db.Uuid tokenHash String @unique @map("token_hash") @db.VarChar(255) sessionId String @map("session_id") @db.Uuid generation Int status RefreshTokenStatus @default(ACTIVE) usedAt DateTime? @map("used_at") @db.Timestamptz(3) replacedByHash String? @unique @map("replaced_by_hash") @db.VarChar(255) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) expiresAt DateTime @map("expires_at") @db.Timestamptz(3) session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade) @@unique([sessionId, generation]) @@index([sessionId, status]) @@index([expiresAt]) @@map("refresh_tokens") } model Bottle { id String @id @default(uuid()) @db.Uuid authorId String @map("author_id") @db.Uuid clientRequestId String @default(uuid()) @map("client_request_id") @db.VarChar(128) contentText String @map("content_text") @db.Text reviewStatus ReviewStatus @default(DRAFT) @map("review_status") poolStatus BottlePoolStatus @default(CLOSED) @map("pool_status") version Int @default(1) approvedAt DateTime? @map("approved_at") @db.Timestamptz(3) consumedAt DateTime? @map("consumed_at") @db.Timestamptz(3) expiresAt DateTime? @map("expires_at") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) author Account @relation(fields: [authorId], references: [id], onDelete: Restrict) leases BottlePickLease[] pickHistory BottlePickHistory[] conversation Conversation? reports Report[] @@unique([authorId, clientRequestId]) @@index([poolStatus, createdAt]) @@index([reviewStatus, createdAt]) @@index([authorId, createdAt]) @@map("bottles") } model BottlePickLease { /// Partial unique index bottle_pick_leases_one_active_per_bottle is managed in 0001_init SQL. id String @id @default(uuid()) @db.Uuid bottleId String @map("bottle_id") @db.Uuid pickerId String @map("picker_id") @db.Uuid leaseTokenHash String @unique @map("lease_token_hash") @db.VarChar(255) expiresAt DateTime @map("expires_at") @db.Timestamptz(3) status BottlePickLeaseStatus @default(ACTIVE) endedAt DateTime? @map("ended_at") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) bottle Bottle @relation(fields: [bottleId], references: [id], onDelete: Cascade) picker Account @relation("PickerLeases", fields: [pickerId], references: [id], onDelete: Cascade) @@index([pickerId, expiresAt]) @@index([bottleId, expiresAt]) @@map("bottle_pick_leases") } model BottlePickHistory { id String @id @default(uuid()) @db.Uuid bottleId String @map("bottle_id") @db.Uuid pickerId String @map("picker_id") @db.Uuid pickedAt DateTime @default(now()) @map("picked_at") @db.Timestamptz(3) bottle Bottle @relation(fields: [bottleId], references: [id], onDelete: Cascade) picker Account @relation("PickerHistory", fields: [pickerId], references: [id], onDelete: Cascade) @@unique([bottleId, pickerId]) @@index([pickerId, pickedAt]) @@map("bottle_pick_history") } model DailyUsage { /// Nonnegative counters are enforced by CHECK constraints in 0001_init SQL. id String @id @default(uuid()) @db.Uuid accountId String @map("account_id") @db.Uuid usageDate DateTime @map("usage_date") @db.Date bottlesCreated Int @default(0) @map("bottles_created") bottlesPicked Int @default(0) @map("bottles_picked") messagesSent Int @default(0) @map("messages_sent") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) @@unique([accountId, usageDate]) @@index([usageDate]) @@map("daily_usage") } model Conversation { id String @id @default(uuid()) @db.Uuid sourceBottleId String @unique @map("source_bottle_id") @db.Uuid status ConversationStatus @default(ACTIVE) nextSeq BigInt @default(1) @map("next_seq") lastMessageAt DateTime @default(now()) @map("last_message_at") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) sourceBottle Bottle @relation(fields: [sourceBottleId], references: [id], onDelete: Restrict) members ConversationMember[] messages Message[] reports Report[] @@index([status, updatedAt]) @@map("conversations") } model ConversationMember { id String @id @default(uuid()) @db.Uuid conversationId String @map("conversation_id") @db.Uuid accountId String @map("account_id") @db.Uuid joinedAt DateTime @default(now()) @map("joined_at") @db.Timestamptz(3) leftAt DateTime? @map("left_at") @db.Timestamptz(3) lastReadSeq BigInt @default(0) @map("last_read_seq") peerAliasSnapshot String @map("peer_alias_snapshot") @db.VarChar(64) blockedAt DateTime? @map("blocked_at") @db.Timestamptz(3) conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) @@unique([conversationId, accountId]) @@index([accountId, joinedAt]) @@map("conversation_members") } model Message { id String @id @default(uuid()) @db.Uuid conversationId String @map("conversation_id") @db.Uuid senderId String @map("sender_id") @db.Uuid clientMsgId String @map("client_msg_id") @db.VarChar(128) seq BigInt contentText String @map("content_text") @db.Text status MessageStatus @default(SENT) reviewStatus ReviewStatus @default(REVIEWING) @map("review_status") sentAt DateTime @default(now()) @map("sent_at") @db.Timestamptz(3) recalledAt DateTime? @map("recalled_at") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) sender Account @relation(fields: [senderId], references: [id], onDelete: Restrict) reports Report[] @@unique([conversationId, clientMsgId]) @@unique([conversationId, seq]) @@index([senderId, createdAt]) @@map("messages") } model Block { id String @id @default(uuid()) @db.Uuid blockerId String @map("blocker_id") @db.Uuid blockedId String @map("blocked_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) blocker Account @relation("Blocker", fields: [blockerId], references: [id], onDelete: Cascade) blocked Account @relation("Blocked", fields: [blockedId], references: [id], onDelete: Cascade) @@unique([blockerId, blockedId]) @@index([blockedId]) @@map("blocks") } model Report { /// Exactly one nullable target foreign key must be set; enforced in 0001_init SQL. id String @id @default(uuid()) @db.Uuid reporterId String @map("reporter_id") @db.Uuid reportedAccountId String? @map("reported_account_id") @db.Uuid bottleId String? @map("bottle_id") @db.Uuid conversationId String? @map("conversation_id") @db.Uuid messageId String? @map("message_id") @db.Uuid reason String @db.VarChar(100) details String? @db.Text targetSnapshot Json @map("target_snapshot") resolution String? @db.Text status ReportStatus @default(PENDING) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) reporter Account @relation("Reporter", fields: [reporterId], references: [id], onDelete: Restrict) reportedAccount Account? @relation("Reported", fields: [reportedAccountId], references: [id], onDelete: SetNull) bottle Bottle? @relation(fields: [bottleId], references: [id], onDelete: SetNull) conversation Conversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull) message Message? @relation(fields: [messageId], references: [id], onDelete: SetNull) moderationTasks ModerationTask[] @@index([status, createdAt]) @@index([reportedAccountId, createdAt]) @@map("reports") } model ModerationTask { id String @id @default(uuid()) @db.Uuid targetType String @map("target_type") @db.VarChar(50) targetId String @map("target_id") @db.Uuid provider String @db.VarChar(100) result Json? riskLabels String[] @map("risk_labels") payloadHash String @map("payload_hash") @db.VarChar(128) reportId String? @map("report_id") @db.Uuid assignedToId String? @map("assigned_to_id") @db.Uuid status ModerationTaskStatus @default(PENDING) decision String? @db.Text createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) reviewedAt DateTime? @map("reviewed_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) report Report? @relation(fields: [reportId], references: [id], onDelete: SetNull) assignedTo Account? @relation("Moderator", fields: [assignedToId], references: [id], onDelete: SetNull) sanctions Sanction[] @@unique([targetType, targetId]) @@index([status, createdAt]) @@index([targetType, targetId]) @@index([assignedToId, status]) @@map("moderation_tasks") } model Sanction { id String @id @default(uuid()) @db.Uuid accountId String @map("account_id") @db.Uuid issuedById String? @map("issued_by_id") @db.Uuid moderationTaskId String? @map("moderation_task_id") @db.Uuid type SanctionType reason String @db.Text startsAt DateTime @default(now()) @map("starts_at") @db.Timestamptz(3) expiresAt DateTime? @map("expires_at") @db.Timestamptz(3) revokedAt DateTime? @map("revoked_at") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) account Account @relation("SanctionedAccount", fields: [accountId], references: [id], onDelete: Cascade) issuedBy Account? @relation("SanctionIssuer", fields: [issuedById], references: [id], onDelete: SetNull) moderationTask ModerationTask? @relation(fields: [moderationTaskId], references: [id], onDelete: SetNull) @@index([accountId, expiresAt]) @@map("sanctions") } model Notification { id String @id @default(uuid()) @db.Uuid accountId String @map("account_id") @db.Uuid type String @db.VarChar(100) payload Json status NotificationStatus @default(PENDING) sentAt DateTime? @map("sent_at") @db.Timestamptz(3) readAt DateTime? @map("read_at") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) @@index([accountId, status, createdAt]) @@map("notifications") } model OutboxEvent { id String @id @default(uuid()) @db.Uuid aggregateType String @map("aggregate_type") @db.VarChar(100) aggregateId String @map("aggregate_id") @db.Uuid eventType String @map("event_type") @db.VarChar(150) dedupeKey String @unique @default(uuid()) @map("dedupe_key") @db.VarChar(255) payload Json status OutboxStatus @default(PENDING) attempts Int @default(0) nextRetryAt DateTime @default(now()) @map("next_retry_at") @db.Timestamptz(3) lockedAt DateTime? @map("locked_at") @db.Timestamptz(3) publishedAt DateTime? @map("published_at") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) @@index([status, nextRetryAt]) @@index([aggregateType, aggregateId]) @@map("outbox_events") } model AuditLog { id String @id @default(uuid()) @db.Uuid actorId String? @map("actor_id") @db.Uuid action String @db.VarChar(150) entityType String @map("entity_type") @db.VarChar(100) entityId String? @map("entity_id") @db.Uuid requestId String? @map("request_id") @db.VarChar(128) ipHash String? @map("ip_hash") @db.VarChar(128) metadata Json? createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) actor Account? @relation("AuditActor", fields: [actorId], references: [id], onDelete: SetNull) @@index([actorId, createdAt]) @@index([entityType, entityId, createdAt]) @@index([requestId]) @@map("audit_logs") }