// Real Estate Agent Platform - Database Schema // Milestone 1 & 2: Foundation, Authentication & Agent Features generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" } // =========================================== // ENUMS // =========================================== enum UserRole { USER AGENT ADMIN SUPER_ADMIN } enum UserStatus { ACTIVE INACTIVE SUSPENDED PENDING_VERIFICATION } enum AuthProvider { LOCAL GOOGLE FACEBOOK TWITTER APPLE } enum FieldType { TEXT // Single line text input TEXTAREA // Multi-line text area CHECKBOX // Single checkbox (Yes/No) CHECKBOX_GROUP // Multiple checkboxes in grid (select multiple) RADIO // Radio buttons (select one) SELECT // Dropdown (select one) MULTI_SELECT // Dropdown with multi-select RANGE // Slider with min/max NUMBER // Number input DATE // Date picker TAG_INPUT // Tag input (add custom tags) FILE // File upload (documents, images) REPEATER // Repeatable group of fields (e.g., certification + years) } enum VerificationStatus { NONE // Agent hasn't uploaded documents PENDING_REVIEW // Documents uploaded, awaiting admin review APPROVED // Admin approved verification REJECTED // Admin rejected verification } enum ConnectionStatus { PENDING ACCEPTED REJECTED } enum MessageType { TEXT FILE IMAGE SYSTEM // For system messages like "connection accepted" } enum MessageStatus { SENT DELIVERED READ } enum SupportChatStatus { OPEN CLOSED } enum SubscriptionStatus { ACTIVE PAST_DUE CANCELED UNPAID TRIALING INCOMPLETE } // =========================================== // USER - Authentication Only // =========================================== model User { id String @id @default(uuid()) email String @unique password String? // Null for social login users role UserRole @default(USER) status UserStatus @default(ACTIVE) emailVerified Boolean @default(false) emailVerifiedAt DateTime? avatar String? // Profile picture URL // Social Auth authProvider AuthProvider @default(LOCAL) googleId String? @unique facebookId String? @unique twitterId String? @unique appleId String? @unique // Two-Factor Authentication twoFactorEnabled Boolean @default(false) twoFactorSecret String? // Encrypted TOTP secret twoFactorBackupCodes String? // JSON array of hashed backup codes twoFactorVerifiedAt DateTime? // When 2FA was enabled // User Preferences notificationPreferences Json? // { email: {...}, push: {...} } privacyPreferences Json? // { privacySettings: {...}, dataSettings: {...} } // Push Notification Tokens fcmTokens Json? // [{ token, device, createdAt }] // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lastLoginAt DateTime? // Online Status (for messaging) isOnline Boolean @default(false) lastSeenAt DateTime? // Relations - Profile based on role userProfile UserProfile? agentProfile AgentProfile? // Auth Relations sessions Session[] // Connection Requests connectionRequests ConnectionRequest[] // Messaging Relations conversations Conversation[] messages Message[] // Notifications notifications Notification[] // Support Chat supportChats SupportChat[] // Subscriptions & Payments subscriptions AgentSubscription[] payments Payment[] // Reports reportsSubmitted UserReport[] @relation("ReportsSubmitted") reportsReceived UserReport[] @relation("ReportsReceived") // Verification actions (admin) verificationActions VerificationHistory[] @relation("VerificationActions") // Audit Log (actor) auditLogs AuditLog[] @@index([email]) @@index([role]) @@index([status]) @@index([isOnline]) @@map("users") } // =========================================== // USER PROFILE - Normal Users // =========================================== model UserProfile { id String @id @default(uuid()) userId String @unique firstName String? lastName String? phone String? avatar String? // Location city String? state String? country String? // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@map("user_profiles") } // =========================================== // AGENT PROFILE - Agents/Subscribers // =========================================== model AgentProfile { id String @id @default(uuid()) userId String @unique slug String @unique agentTypeId String? // Basic Info firstName String? lastName String? phone String? avatar String? bio String? @db.Text headline String? // Location city String? state String? country String? address String? zipCode String? latitude Float? longitude Float? // Professional Info yearsOfExperience Int? licenseNumber String? companyName String? website String? // Social Links facebookUrl String? twitterUrl String? linkedinUrl String? instagramUrl String? // Verification Status isVerified Boolean @default(false) verificationStatus VerificationStatus @default(NONE) verificationNote String? // Admin note (rejection reason) verifiedAt DateTime? verifiedBy String? // Admin user ID who verified // Profile Status isProfileComplete Boolean @default(false) profileCompleteness Int @default(0) isPublic Boolean @default(true) isFeatured Boolean @default(false) isAvailable Boolean @default(true) // Agent availability status for connect requests // Subscription (denormalized for quick access) subscriptionStatus String? // "ACTIVE", "NONE", etc. // Stats (denormalized for performance) totalReviews Int @default(0) averageRating Float @default(0) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) agentType AgentType? @relation(fields: [agentTypeId], references: [id]) fieldValues AgentProfileFieldValue[] // Testimonial Link testimonialToken String? @unique connectionRequests ConnectionRequest[] conversations Conversation[] testimonials Testimonial[] @@index([userId]) @@index([agentTypeId]) @@index([slug]) @@index([city, state]) // Verification History verificationHistory VerificationHistory[] @@index([isVerified]) @@index([verificationStatus]) @@index([isPublic]) @@index([isFeatured]) @@index([isAvailable]) @@map("agent_profiles") } // =========================================== // AGENT TYPE - Dynamic agent types (Professional, Lender, etc.) // =========================================== model AgentType { id String @id @default(uuid()) name String @unique description String? icon String? isActive Boolean @default(true) sortOrder Int @default(0) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations agents AgentProfile[] agentTypeSections AgentTypeSection[] @@index([isActive]) @@map("agent_types") } // =========================================== // DYNAMIC PROFILE FIELD SYSTEM // =========================================== // Profile Sections - Groups of related fields model ProfileSection { id String @id @default(uuid()) name String // e.g., "Location", "Experience", "Specialization" slug String @unique // URL-friendly identifier description String? icon String? // Icon name or URL sortOrder Int @default(0) isActive Boolean @default(true) isGlobal Boolean @default(false) // If true, applies to ALL agent types isSystem Boolean @default(false) // If true, section cannot be deleted (system default) isRepeatable Boolean @default(false) // If true, user can add multiple entries (e.g., certifications) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations fields ProfileField[] agentTypeSections AgentTypeSection[] @@index([isActive]) @@index([isSystem]) @@index([sortOrder]) @@map("profile_sections") } // Many-to-Many: Which sections belong to which agent types model AgentTypeSection { id String @id @default(uuid()) agentTypeId String sectionId String sortOrder Int @default(0) // Order specific to this agent type isRequired Boolean @default(false) // Is this section required for this type // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations agentType AgentType @relation(fields: [agentTypeId], references: [id], onDelete: Cascade) section ProfileSection @relation(fields: [sectionId], references: [id], onDelete: Cascade) @@unique([agentTypeId, sectionId]) @@index([agentTypeId]) @@index([sectionId]) @@map("agent_type_sections") } // Profile Fields - Individual form fields model ProfileField { id String @id @default(uuid()) sectionId String name String // e.g., "State", "Years in Business" slug String // Unique within section: e.g., "state", "years_in_business" fieldType FieldType description String? // Help text shown to user placeholder String? // Placeholder text for input defaultValue String? // Default value (JSON for complex types) sortOrder Int @default(0) isActive Boolean @default(true) isRequired Boolean @default(false) isSearchableOnly Boolean @default(false) // If true, shown in edit form & search, but hidden on public profile // Validation rules (JSON) validation Json? // { min, max, minLength, maxLength, pattern, etc. } // For SELECT, RADIO, MULTI_SELECT, CHECKBOX_GROUP options Json? // [{ value: "...", label: "...", sortOrder: 0 }] // For RANGE/slider rangeConfig Json? // { min: 0, max: 100, step: 1 } // UI Configuration uiConfig Json? // { columns: 2, showInPreview: true, etc. } // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations section ProfileSection @relation(fields: [sectionId], references: [id], onDelete: Cascade) fieldValues AgentProfileFieldValue[] @@unique([sectionId, slug]) @@index([sectionId]) @@index([isActive]) @@map("profile_fields") } // Agent Profile Field Values - Stores actual agent data model AgentProfileFieldValue { id String @id @default(uuid()) agentProfileId String fieldId String // Value storage - use appropriate column based on field type textValue String? @db.Text // For TEXT, TEXTAREA numberValue Float? // For NUMBER, RANGE booleanValue Boolean? // For single CHECKBOX jsonValue Json? // For MULTI_SELECT, RADIO, complex data dateValue DateTime? // For DATE // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade) field ProfileField @relation(fields: [fieldId], references: [id], onDelete: Cascade) @@unique([agentProfileId, fieldId]) @@index([agentProfileId]) @@index([fieldId]) @@map("agent_profile_field_values") } // =========================================== // SESSION - JWT Token Management // =========================================== model Session { id String @id @default(uuid()) userId String token String @unique refreshToken String? @unique userAgent String? ipAddress String? expiresAt DateTime createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([token]) @@map("sessions") } // =========================================== // CONNECTION REQUEST - User to Agent connections // =========================================== model ConnectionRequest { id String @id @default(uuid()) userId String // User sending the request agentProfileId String // Agent receiving the request status ConnectionStatus @default(PENDING) message String? @db.Text // Optional message from user // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt respondedAt DateTime? // When agent responded // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade) @@unique([userId, agentProfileId]) // One request per user-agent pair @@index([agentProfileId, status]) @@index([userId, status]) @@index([status]) @@map("connection_requests") } // =========================================== // MESSAGING SYSTEM // =========================================== model Conversation { id String @id @default(uuid()) userId String // Regular user in the conversation agentProfileId String // Agent in the conversation lastMessageAt DateTime? lastMessageText String? @db.VarChar(255) userUnreadCount Int @default(0) // Unread count for the user agentUnreadCount Int @default(0) // Unread count for the agent // Mute & Favorite (per-user) userMuted Boolean @default(false) agentMuted Boolean @default(false) userFavorited Boolean @default(false) agentFavorited Boolean @default(false) // Clear chat (per-user) — messages before this timestamp are hidden for that user userClearedAt DateTime? agentClearedAt DateTime? // Soft delete (per-user) — conversation hidden for that user until a new message arrives userDeletedAt DateTime? agentDeletedAt DateTime? // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade) messages Message[] @@unique([userId, agentProfileId]) // One conversation per user-agent pair @@index([userId, lastMessageAt]) @@index([agentProfileId, lastMessageAt]) @@map("conversations") } model Message { id String @id @default(uuid()) conversationId String senderId String // User ID of the sender (can be user or agent's user) content String @db.Text messageType MessageType @default(TEXT) // File attachment fields fileUrl String? fileName String? fileSize Int? // File size in bytes mimeType String? // Message status status MessageStatus @default(SENT) deliveredAt DateTime? readAt DateTime? // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) sender User @relation(fields: [senderId], references: [id], onDelete: Cascade) @@index([conversationId, createdAt]) @@index([senderId]) @@index([status]) @@map("messages") } // =========================================== // IN-APP NOTIFICATIONS // =========================================== model Notification { id String @id @default(uuid()) userId String type String // 'connection', 'message', 'system', 'update', 'request' title String description String read Boolean @default(false) actionUrl String? data Json? // Extra metadata (conversationId, connectionRequestId, etc.) // Timestamps createdAt DateTime @default(now()) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, read]) @@index([userId, createdAt]) @@map("notifications") } // =========================================== // CMS CONTENT - Dynamic page content management // =========================================== model CmsContent { id String @id @default(uuid()) pageSlug String sectionKey String content Json isPublished Boolean @default(true) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([pageSlug, sectionKey]) @@index([pageSlug]) @@map("cms_contents") } // =========================================== // TESTIMONIALS // =========================================== model Testimonial { id String @id @default(uuid()) agentProfileId String rating Int // 1-5 text String @db.Text authorName String authorRole String // "Home Buyer", "Investor", etc. isPublished Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade) @@index([agentProfileId]) @@map("testimonials") } // =========================================== // SUPPORT CHAT SYSTEM // =========================================== model SupportChat { id String @id @default(uuid()) userId String status SupportChatStatus @default(OPEN) lastMessageAt DateTime? lastMessageText String? @db.VarChar(255) userUnreadCount Int @default(0) adminUnreadCount Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) messages SupportMessage[] @@index([userId]) @@index([status]) @@map("support_chats") } model SupportMessage { id String @id @default(uuid()) chatId String senderId String senderRole String // "USER" or "ADMIN" content String @db.Text createdAt DateTime @default(now()) chat SupportChat @relation(fields: [chatId], references: [id], onDelete: Cascade) @@index([chatId]) @@map("support_messages") } // =========================================== // SUBSCRIPTION & PAYMENTS // =========================================== model SubscriptionPlan { id String @id @default(uuid()) name String // "Professional Annual" description String? @db.Text stripePriceId String @unique // Stripe Price ID (price_xxx) amount Int // Amount in cents (49900) currency String @default("usd") interval String @default("year") // "month" | "year" features Json? // ["Feature 1", "Feature 2"] isActive Boolean @default(true) sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt subscriptions AgentSubscription[] @@index([isActive]) @@map("subscription_plans") } model AgentSubscription { id String @id @default(uuid()) userId String planId String stripeCustomerId String // cus_xxx stripeSubscriptionId String? @unique // sub_xxx status SubscriptionStatus @default(INCOMPLETE) currentPeriodStart DateTime? currentPeriodEnd DateTime? cancelAtPeriodEnd Boolean @default(false) canceledAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) plan SubscriptionPlan @relation(fields: [planId], references: [id]) payments Payment[] @@index([userId]) @@index([status]) @@index([stripeCustomerId]) @@map("agent_subscriptions") } model Payment { id String @id @default(uuid()) subscriptionId String? userId String stripePaymentIntentId String? @unique // pi_xxx stripeInvoiceId String? @unique // in_xxx amount Int // cents currency String @default("usd") status String // "succeeded", "failed", "pending" receiptUrl String? createdAt DateTime @default(now()) subscription AgentSubscription? @relation(fields: [subscriptionId], references: [id]) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([subscriptionId]) @@index([status]) @@map("payments") } // ============================================ // USER REPORTS // ============================================ enum ReportStatus { PENDING REVIEWED RESOLVED DISMISSED } model UserReport { id String @id @default(uuid()) reporterId String reportedUserId String conversationId String? reason String description String? status ReportStatus @default(PENDING) adminNotes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt reporter User @relation("ReportsSubmitted", fields: [reporterId], references: [id], onDelete: Cascade) reportedUser User @relation("ReportsReceived", fields: [reportedUserId], references: [id], onDelete: Cascade) @@index([reporterId]) @@index([reportedUserId]) @@index([status]) @@map("user_reports") } // ============================================ // CONTACT MESSAGES // ============================================ model ContactMessage { id String @id @default(uuid()) name String email String phone String? message String isRead Boolean @default(false) createdAt DateTime @default(now()) @@index([isRead]) @@index([createdAt]) @@map("contact_messages") } // ============================================ // VERIFICATION HISTORY // ============================================ model VerificationHistory { id String @id @default(uuid()) agentProfileId String status VerificationStatus note String? adminId String? submittedData Json? // Snapshot of agent profile + documents at submission time createdAt DateTime @default(now()) agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade) admin User? @relation("VerificationActions", fields: [adminId], references: [id]) @@index([agentProfileId]) @@index([createdAt]) @@map("verification_history") } // ============================================ // AUDIT LOG // ============================================ model AuditLog { id String @id @default(uuid()) actorId String? // null = system / unauthenticated actorRole String? // USER, AGENT, ADMIN, SUPER_ADMIN, SYSTEM action String // see AuditAction enum in audit.constants.ts resourceType String? // User, AgentProfile, Subscription, Payment, etc. resourceId String? metadata Json? // { before, after, reason, params, ... } ipAddress String? userAgent String? createdAt DateTime @default(now()) actor User? @relation(fields: [actorId], references: [id], onDelete: SetNull) @@index([actorId, createdAt]) @@index([resourceType, resourceId]) @@index([action, createdAt]) @@index([createdAt]) @@map("audit_logs") }