// Novya backend schema. // Identity/auth from Phase 3 is preserved. Domain tables match BACKEND_DATABASE_ARCHITECTURE_AUDIT.md. generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } enum AuthProvider { password guest } enum AttemptPhase { pre post } enum AttemptStatus { /// Reserved for future draft/in-progress attempts; MVP always writes `scored`. submitted scored } enum SessionProgressStatus { not_started in_progress completed } enum ChatRole { /// Used in coach prompts at runtime; not persisted to chat_messages. system user assistant } enum AiRunStatus { queued running succeeded failed } model User { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt externalId String @unique @default(uuid()) @db.Char(36) isGuest Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt profile UserProfile? identities AuthIdentity[] sessions AuthSession[] journey LearningJourney? assessmentAttempts AssessmentAttempt[] sessionProgress SessionProgress[] chatSessions ChatSession[] aiRuns AiAnalysisRun[] @@index([createdAt], map: "idx_users_created_at") @@index([isGuest], map: "idx_users_is_guest") @@map("users") } model UserProfile { userId BigInt @id @db.UnsignedBigInt name String @db.VarChar(100) age Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([name], map: "idx_user_profiles_name") @@map("user_profiles") } model AuthIdentity { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt userId BigInt @db.UnsignedBigInt provider AuthProvider providerSubject String @db.VarChar(191) email String? @db.VarChar(191) passwordHash String? @db.VarChar(255) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerSubject], map: "uq_auth_identities_provider_subject") @@index([userId], map: "idx_auth_identities_user_id") @@index([email], map: "idx_auth_identities_email") @@map("auth_identities") } model AuthSession { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt userId BigInt @db.UnsignedBigInt tokenHash String @unique @db.Char(64) expiresAt DateTime revokedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, expiresAt], map: "idx_auth_sessions_user_id_expires_at") @@index([expiresAt], map: "idx_auth_sessions_expires_at") @@map("auth_sessions") } /// Timestamps are authoritative. currentStep is a denormalized cache. model LearningJourney { userId BigInt @id @db.UnsignedBigInt currentStep String @default("registered") @db.VarChar(32) preTestCompletedAt DateTime? trainingStartedAt DateTime? trainingCompletedAt DateTime? postTestCompletedAt DateTime? lastActivityAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("learning_journeys") } /// Catalog of the 11 educational sessions. Content blocks are JSON (same shape as PWA). model CourseSession { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt sessionKey String @unique @db.VarChar(64) order Int @unique title String @db.VarChar(191) description String @db.Text estimatedDurationMinutes Int blocks Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt sessionProgress SessionProgress[] chatSessions ChatSession[] @@index([order], map: "idx_course_sessions_order") @@map("course_sessions") } model SessionProgress { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt userId BigInt @db.UnsignedBigInt sessionOrder Int status SessionProgressStatus @default(not_started) startedAt DateTime? completedAt DateTime? completionMetadata Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) courseSession CourseSession @relation(fields: [sessionOrder], references: [order], onDelete: Restrict, onUpdate: Cascade) @@unique([userId, sessionOrder], map: "uq_session_progress_user_order") @@index([userId, status], map: "idx_session_progress_user_status") @@map("session_progress") } model AssessmentAttempt { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt userId BigInt @db.UnsignedBigInt phase AttemptPhase status AttemptStatus @default(scored) submittedAt DateTime scoredAt DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) answers AssessmentAnswer[] scores AssessmentScore[] @@unique([userId, phase], map: "uq_assessment_attempts_user_phase") @@index([userId, phase, submittedAt], map: "idx_assessment_attempts_user_phase_submitted") @@map("assessment_attempts") } model AssessmentAnswer { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt attemptId BigInt @db.UnsignedBigInt questionId String @db.VarChar(32) rawValue Int points Int dimension String @db.VarChar(32) isReverseScored Boolean createdAt DateTime @default(now()) attempt AssessmentAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade) @@unique([attemptId, questionId], map: "uq_assessment_answers_attempt_question") @@map("assessment_answers") } model AssessmentScore { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt attemptId BigInt @db.UnsignedBigInt metricKey String @db.VarChar(32) rawScore Int createdAt DateTime @default(now()) attempt AssessmentAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade) @@unique([attemptId, metricKey], map: "uq_assessment_scores_attempt_metric") @@map("assessment_scores") } model ChatSession { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt userId BigInt @db.UnsignedBigInt sessionOrder Int startedAt DateTime @default(now()) endedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) courseSession CourseSession @relation(fields: [sessionOrder], references: [order], onDelete: Restrict, onUpdate: Cascade) messages ChatMessage[] aiRuns AiAnalysisRun[] @@index([userId, sessionOrder, startedAt], map: "idx_chat_sessions_user_order_started") @@map("chat_sessions") } model ChatMessage { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt chatSessionId BigInt @db.UnsignedBigInt role ChatRole content String @db.Text metadata Json? createdAt DateTime @default(now()) chatSession ChatSession @relation(fields: [chatSessionId], references: [id], onDelete: Cascade) @@index([chatSessionId, createdAt], map: "idx_chat_messages_session_created") @@map("chat_messages") } model AiAnalysisRun { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt userId BigInt @db.UnsignedBigInt chatSessionId BigInt? provider String @db.VarChar(64) model String @db.VarChar(128) status AiRunStatus @default(queued) latencyMs Int? totalTokens Int? errorMessage String? @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) chatSession ChatSession? @relation(fields: [chatSessionId], references: [id], onDelete: SetNull) @@index([userId, createdAt], map: "idx_ai_analysis_runs_user_created") @@map("ai_analysis_runs") }