// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init generator client { provider = "prisma-client" output = "../src/lib/generated/prisma" } datasource db { provider = "postgresql" } model User { id String @id name String email String emailVerified Boolean @default(false) image String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt roleId Int? @default(4) // New users default to "User" role UserRole? @relation(fields: [roleId], references: [id]) sessions Session[] accounts Account[] profiles Profile[] licenseKeys LicenseKey[] userProclamations UserProclamations[] schedule Schedule? @@unique([email]) @@map("user") } // I chose to create a Profile table here because honestly, I didn't want to waste // a bunch of time trying to figure out how to customize the default Better-Auth model. model Profile { id String @id @default(uuid()) userId String? user User? @relation(fields: [userId], references: [id], onDelete: Cascade) email String? firstName String lastName String phoneNumber String ghlContactId String? @map("ghl_contact_id") addresses Address[] @@index([userId]) @@index([email]) } model Address { id String @id @default(uuid()) profileId String profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) street String city String state String postalCode String country String type String @default("Mailing") isDefaultMailing Boolean @default(false) isDefaultBilling Boolean @default(false) isDefaultShipping Boolean @default(false) } model Session { id String @id expiresAt DateTime token String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt ipAddress String? userAgent String? userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([token]) @@index([userId]) @@map("session") } model Account { id String @id accountId String providerId String userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) accessToken String? refreshToken String? idToken String? accessTokenExpiresAt DateTime? refreshTokenExpiresAt DateTime? scope String? password String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) @@map("account") } model UserRole { id Int @id @default(autoincrement()) name String users User[] } model Verification { id String @id identifier String value String expiresAt DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([identifier]) @@map("verification") } model Proclamation { id String @id @default(uuid()) name String @unique content String solfeggioFrequencyId Int solfeggioFrequency SolfeggioFrequency @relation(fields: [solfeggioFrequencyId], references: [id]) categoryId Int category ProclamationCategory @relation(fields: [categoryId], references: [id]) groupId Int group ProclamationGroup @relation(fields: [groupId], references: [id]) userProclamations UserProclamations[] blockProclamations BlockProclamations[] // Legacy subscription-level FK. Retained in the database but unmanaged; the // FK constraint is dropped and the value is left NULL. Hidden from the client. subscriptionLevelId Int? @ignore } model ProclamationGroup { id Int @id @default(autoincrement()) name String @unique sortOrder Int @default(0) proclamations Proclamation[] } model UserProclamations { userId String user User @relation(fields: [userId], references: [id]) proclamationId String proclamation Proclamation @relation(fields: [proclamationId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([userId, proclamationId]) } model SolfeggioFrequency { id Int @id @default(autoincrement()) name String @unique value Int @unique proclamations Proclamation[] } model ProclamationCategory { id Int @id @default(autoincrement()) name String @unique sortOrder Int @default(0) proclamations Proclamation[] } // Archive of the former child sub-categories removed when the // ProclamationCategory hierarchy was flattened. Kept for recovery/audit. // `@@ignore` keeps it out of the generated client while Prisma remains aware // of the table so `migrate dev` does not report it as an untracked table. model ProclamationCategoryLegacy { id Int @id name String parentId Int? sortOrder Int @default(0) @@map("ProclamationCategoryLegacy") @@ignore } // Archive of the category->subscription-level mappings for the child // categories removed when the category hierarchy was flattened. Kept with // `@@ignore` so Prisma remains aware of the table but it is excluded from the // generated client and Prisma's own migrations. model ProclamationCategorySubscriptionLevelLegacy { categoryId Int subscriptionLevelId Int @@id([categoryId, subscriptionLevelId]) @@map("ProclamationCategorySubscriptionLevelLegacy") @@ignore } model LicenseKey { id String @id @default(uuid()) key String @unique active Boolean createdAt DateTime @default(now()) lastValidatedAt DateTime? userId String? user User? @relation(fields: [userId], references: [id], onDelete: SetNull) email String? // Human-readable label set only by the Kiosk (e.g. "Living Room"). Trimmed, // 1-40 chars. Never a reference or foreign key; the License Key is the only // identifier. `systemNameLower` is a case-folded copy used to enforce // per-account, case-insensitive uniqueness when non-null. systemName String? systemNameLower String? // Legacy subscription level. Retained in the database but unmanaged; hidden // from the client and left NULL. level String? @ignore @@unique([userId, systemNameLower]) @@index([userId]) @@index([email]) } model ApiKey { id String @id @default(cuid()) keyPrefix String keyHash String @unique createdAt DateTime @default(now()) expiresAt DateTime? revokedAt DateTime? } // Portal-editable 7-color palette per canonical solfeggio frequency id (1-9). // `solfeggioFrequencyId` is the canonical id the kiosk uses (see // v2-serializers.ts), not the DB autoincrement id of `SolfeggioFrequency`. model ColorPalette { solfeggioFrequencyId Int @id colors Json // ["#RRGGBB", ... x7] @@map("ColorPalette") } // Append-only audit trail of database mutations. Written centrally by the // Prisma extension in src/lib/db.ts; read by the admin Audit Log page. model AuditLog { id String @id @default(uuid()) createdAt DateTime @default(now()) userId String? email String? roleId Int? roleName String? page String action String entity String? entityId String? before Json? after Json? ip String? @@index([createdAt]) @@index([roleId]) @@index([action]) @@index([page]) @@map("AuditLog") } // A user's daily listening schedule. Exactly one per user (enforced by the // unique userId); contains 0-4 time Blocks. Times are wall-clock minutes from // midnight (0-1440), interpreted by the kiosk in the user's configured // timezone. The portal never stores a timezone. model Schedule { id String @id @default(uuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) blocks Block[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // A single non-overlapping time range within a Schedule, plus the ordered set // of selected Proclamations to play during it. Overnight ranges are disallowed // in v1, so startMinutes is always less than endMinutes. model Block { id String @id @default(uuid()) scheduleId String schedule Schedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade) startMinutes Int endMinutes Int // When true, the kiosk enables dynamic audio for the duration of this block, // overriding any audio selections made on the device. dynamicAudio Boolean @default(false) proclamations BlockProclamations[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([scheduleId]) } // Join table: the Proclamations selected for a Block. Carries the Proclamation // content for playback via the API; the portal UI only ever reads // title/category/group. model BlockProclamations { blockId String block Block @relation(fields: [blockId], references: [id], onDelete: Cascade) proclamationId String proclamation Proclamation @relation(fields: [proclamationId], references: [id]) sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([blockId, proclamationId]) @@index([proclamationId]) }