generator client { provider = "prisma-client-js" binaryTargets = ["native", "debian-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] previewFeatures = ["metrics", "tracing", "relationJoins", "nativeDistinct"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(uuid()) @db.VarChar name String @db.VarChar email String @unique @db.VarChar emailVerifiedAt DateTime? @map("email_verified") @db.Timestamptz(3) avatarUrl String? @map("avatar_url") @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) /// Not available if user signed up through OAuth providers password String? @db.VarChar /// Indicate whether the user finished the signup progress. /// for example, the value will be false if user never registered and invited into a workspace by others. registered Boolean @default(true) features UserFeature[] userStripeCustomer UserStripeCustomer? workspacePermissions WorkspaceUserPermission[] pagePermissions WorkspacePageUserPermission[] connectedAccounts ConnectedAccount[] sessions UserSession[] aiSessions AiSession[] updatedRuntimeConfigs RuntimeConfig[] userSnapshots UserSnapshot[] createdSnapshot Snapshot[] @relation("createdSnapshot") updatedSnapshot Snapshot[] @relation("updatedSnapshot") createdUpdate Update[] @relation("createdUpdate") createdHistory SnapshotHistory[] @relation("createdHistory") @@index([email]) @@map("users") } model ConnectedAccount { id String @id @default(uuid()) @db.VarChar userId String @map("user_id") @db.VarChar provider String @db.VarChar providerAccountId String @map("provider_account_id") @db.VarChar scope String? @db.Text accessToken String? @map("access_token") @db.Text refreshToken String? @map("refresh_token") @db.Text 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) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([providerAccountId]) @@map("user_connected_accounts") } model Session { id String @id @default(uuid()) @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) userSessions UserSession[] // @deprecated use [UserSession.expiresAt] deprecated_expiresAt DateTime? @map("expires_at") @db.Timestamptz(3) @@map("multiple_users_sessions") } model UserSession { id String @id @default(uuid()) @db.VarChar sessionId String @map("session_id") @db.VarChar userId String @map("user_id") @db.VarChar expiresAt DateTime? @map("expires_at") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([sessionId, userId]) @@map("user_sessions") } model VerificationToken { token String @db.VarChar type Int @db.SmallInt credential String? @db.Text expiresAt DateTime @db.Timestamptz(3) @@unique([type, token]) @@map("verification_tokens") } model Workspace { id String @id @default(uuid()) @db.VarChar public Boolean createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) // workspace level feature flags enableAi Boolean @default(true) @map("enable_ai") enableUrlPreview Boolean @default(false) @map("enable_url_preview") pages WorkspacePage[] permissions WorkspaceUserPermission[] pagePermissions WorkspacePageUserPermission[] features WorkspaceFeature[] blobs Blob[] @@map("workspaces") } // Table for workspace page meta data // NOTE: // We won't make sure every page has a corresponding record in this table. // Only the ones that have ever changed will have records here, // and for others we will make sure it's has a default value return in our bussiness logic. model WorkspacePage { workspaceId String @map("workspace_id") @db.VarChar pageId String @map("page_id") @db.VarChar public Boolean @default(false) // Page/Edgeless mode Int @default(0) @db.SmallInt workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@id([workspaceId, pageId]) @@map("workspace_pages") } enum WorkspaceMemberStatus { Pending // 1. old state accepted = false NeedMoreSeat // 2.1 for team: workspace owner need to buy more seat NeedMoreSeatAndReview // 2.2 for team: workspace owner need to buy more seat and member need review UnderReview // 3. for team: member is under review Accepted // 4. old state accepted = true } model WorkspaceUserPermission { id String @id @default(uuid()) @db.VarChar workspaceId String @map("workspace_id") @db.VarChar userId String @map("user_id") @db.VarChar // Read/Write type Int @db.SmallInt /// @deprecated Whether the permission invitation is accepted by the user accepted Boolean @default(false) /// Whether the invite status of the workspace member status WorkspaceMemberStatus @default(Pending) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) /// When the invite status changed updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3) user User @relation(fields: [userId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@unique([workspaceId, userId]) // optimize for querying user's workspace permissions @@index(userId) @@map("workspace_user_permissions") } model WorkspacePageUserPermission { id String @id @default(uuid()) @db.VarChar workspaceId String @map("workspace_id") @db.VarChar pageId String @map("page_id") @db.VarChar userId String @map("user_id") @db.VarChar // Read/Write type Int @db.SmallInt /// Whether the permission invitation is accepted by the user accepted Boolean @default(false) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) user User @relation(fields: [userId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@unique([workspaceId, pageId, userId]) @@map("workspace_page_user_permissions") } // feature gates is a way to enable/disable features for a user // for example: // - early access is a feature that allow some users to access the insider version // - pro plan is a quota that allow some users access to more resources after they pay model UserFeature { id Int @id @default(autoincrement()) userId String @map("user_id") @db.VarChar featureId Int @map("feature_id") @db.Integer // we will record the reason why the feature is enabled/disabled // for example: // - pro_plan_v1: "user buy the pro plan" reason String @db.VarChar // record the quota enabled time createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) // record the quota expired time, pay plan is a subscription, so it will expired expiredAt DateTime? @map("expired_at") @db.Timestamptz(3) // whether the feature is activated // for example: // - if we switch the user to another plan, we will set the old plan to deactivated, but dont delete it activated Boolean @default(false) feature Feature @relation(fields: [featureId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@map("user_features") } // feature gates is a way to enable/disable features for a workspace // for example: // - copilet is a feature that allow some users in a workspace to access the copilet feature model WorkspaceFeature { id Int @id @default(autoincrement()) workspaceId String @map("workspace_id") @db.VarChar featureId Int @map("feature_id") @db.Integer // override quota's configs configs Json @default("{}") @db.Json // we will record the reason why the feature is enabled/disabled // for example: // - copilet_v1: "owner buy the copilet feature package" reason String @db.VarChar // record the feature enabled time createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) // record the quota expired time, pay plan is a subscription, so it will expired expiredAt DateTime? @map("expired_at") @db.Timestamptz(3) // whether the feature is activated // for example: // - if owner unsubscribe a feature package, we will set the feature to deactivated, but dont delete it activated Boolean @default(false) feature Feature @relation(fields: [featureId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@index([workspaceId]) @@map("workspace_features") } model Feature { id Int @id @default(autoincrement()) feature String @db.VarChar version Int @default(0) @db.Integer // 0: feature, 1: quota type Int @db.Integer // configs, define by feature controller configs Json @db.Json createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) UserFeatureGates UserFeature[] WorkspaceFeatures WorkspaceFeature[] @@unique([feature, version]) @@map("features") } // the latest snapshot of each doc that we've seen // Snapshot + Updates are the latest state of the doc model Snapshot { workspaceId String @map("workspace_id") @db.VarChar id String @default(uuid()) @map("guid") @db.VarChar blob Bytes @db.ByteA state Bytes? @db.ByteA createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) // the `updated_at` field will not record the time of record changed, // but the created time of last seen update that has been merged into snapshot. updatedAt DateTime @map("updated_at") @db.Timestamptz(3) createdBy String? @map("created_by") @db.VarChar updatedBy String? @map("updated_by") @db.VarChar // should not delete origin snapshot even if user is deleted // we only delete the snapshot if the workspace is deleted createdByUser User? @relation(name: "createdSnapshot", fields: [createdBy], references: [id], onDelete: SetNull) updatedByUser User? @relation(name: "updatedSnapshot", fields: [updatedBy], references: [id], onDelete: SetNull) // @deprecated use updatedAt only seq Int? @default(0) @db.Integer // we need to clear all hanging updates and snapshots before enable the foreign key on workspaceId // workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@id([workspaceId, id]) @@index([workspaceId, updatedAt]) @@map("snapshots") } // user snapshots are special snapshots for user storage like personal app settings, distinguished from workspace snapshots // basically they share the same structure with workspace snapshots // but for convenience, we don't fork the updates queue and hisotry for user snapshots, until we have to // which means all operation on user snapshot will happen in-pace model UserSnapshot { userId String @map("user_id") @db.VarChar id String @map("id") @db.VarChar blob Bytes @db.ByteA createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@id([userId, id]) @@map("user_snapshots") } model Update { workspaceId String @map("workspace_id") @db.VarChar id String @map("guid") @db.VarChar blob Bytes @db.ByteA createdAt DateTime @map("created_at") @db.Timestamptz(3) createdBy String? @map("created_by") @db.VarChar // will delete createor record if createor's account is deleted createdByUser User? @relation(name: "createdUpdate", fields: [createdBy], references: [id], onDelete: SetNull) // @deprecated use createdAt only seq Int? @db.Integer @@id([workspaceId, id, createdAt]) @@map("updates") } model SnapshotHistory { workspaceId String @map("workspace_id") @db.VarChar id String @map("guid") @db.VarChar timestamp DateTime @db.Timestamptz(3) blob Bytes @db.ByteA state Bytes? @db.ByteA expiredAt DateTime @map("expired_at") @db.Timestamptz(3) createdBy String? @map("created_by") @db.VarChar // will delete createor record if creator's account is deleted createdByUser User? @relation(name: "createdHistory", fields: [createdBy], references: [id], onDelete: SetNull) @@id([workspaceId, id, timestamp]) @@map("snapshot_histories") } enum AiPromptRole { system assistant user } model AiPromptMessage { promptId Int @map("prompt_id") @db.Integer // if a group of prompts contains multiple sentences, idx specifies the order of each sentence idx Int @db.Integer // system/assistant/user role AiPromptRole // prompt content content String @db.Text attachments Json? @db.Json params Json? @db.Json createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) prompt AiPrompt @relation(fields: [promptId], references: [id], onDelete: Cascade) @@unique([promptId, idx]) @@map("ai_prompts_messages") } model AiPrompt { id Int @id @default(autoincrement()) @db.Integer name String @unique @db.VarChar(32) // an mark identifying which view to use to display the session // it is only used in the frontend and does not affect the backend action String? @db.VarChar model String @db.VarChar config Json? @db.Json createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3) // whether the prompt is modified by the admin panel modified Boolean @default(false) messages AiPromptMessage[] sessions AiSession[] @@map("ai_prompts_metadata") } model AiSessionMessage { id String @id @default(uuid()) @db.VarChar sessionId String @map("session_id") @db.VarChar role AiPromptRole content String @db.Text attachments Json? @db.Json params Json? @db.Json createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) session AiSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) @@index([sessionId]) @@map("ai_sessions_messages") } model AiSession { id String @id @default(uuid()) @db.VarChar userId String @map("user_id") @db.VarChar workspaceId String @map("workspace_id") @db.VarChar docId String @map("doc_id") @db.VarChar promptName String @map("prompt_name") @db.VarChar(32) // the session id of the parent session if this session is a forked session parentSessionId String? @map("parent_session_id") @db.VarChar messageCost Int @default(0) tokenCost Int @default(0) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3) user User @relation(fields: [userId], references: [id], onDelete: Cascade) prompt AiPrompt @relation(fields: [promptName], references: [name], onDelete: Cascade) messages AiSessionMessage[] @@index([userId]) @@index([userId, workspaceId]) @@map("ai_sessions_metadata") } model DataMigration { id String @id @default(uuid()) @db.VarChar name String @db.VarChar startedAt DateTime @default(now()) @map("started_at") @db.Timestamptz(3) finishedAt DateTime? @map("finished_at") @db.Timestamptz(3) @@map("_data_migrations") } enum RuntimeConfigType { String Number Boolean Object Array } model RuntimeConfig { id String @id @db.VarChar type RuntimeConfigType module String @db.VarChar key String @db.VarChar value Json @db.Json description String @db.Text updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3) lastUpdatedBy String? @map("last_updated_by") @db.VarChar lastUpdatedByUser User? @relation(fields: [lastUpdatedBy], references: [id]) @@unique([module, key]) @@map("app_runtime_settings") } model DeprecatedUserSubscription { id Int @id @default(autoincrement()) @db.Integer userId String @map("user_id") @db.VarChar plan String @db.VarChar(20) // yearly/monthly/lifetime recurring String @db.VarChar(20) // onetime subscription or anything else variant String? @db.VarChar(20) // subscription.id, null for lifetime payment or one time payment subscription stripeSubscriptionId String? @unique @map("stripe_subscription_id") // subscription.status, active/past_due/canceled/unpaid... status String @db.VarChar(20) // subscription.current_period_start start DateTime @map("start") @db.Timestamptz(3) // subscription.current_period_end, null for lifetime payment end DateTime? @map("end") @db.Timestamptz(3) // subscription.billing_cycle_anchor nextBillAt DateTime? @map("next_bill_at") @db.Timestamptz(3) // subscription.canceled_at canceledAt DateTime? @map("canceled_at") @db.Timestamptz(3) // subscription.trial_start trialStart DateTime? @map("trial_start") @db.Timestamptz(3) // subscription.trial_end trialEnd DateTime? @map("trial_end") @db.Timestamptz(3) stripeScheduleId String? @map("stripe_schedule_id") @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) @@unique([userId, plan]) @@map("user_subscriptions") } model DeprecatedUserInvoice { id Int @id @default(autoincrement()) @db.Integer userId String @map("user_id") @db.VarChar stripeInvoiceId String @unique @map("stripe_invoice_id") currency String @db.VarChar(3) // CNY 12.50 stored as 1250 amount Int @db.Integer status String @db.VarChar(20) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) // billing reason reason String? @db.VarChar lastPaymentError String? @map("last_payment_error") @db.Text // stripe hosted invoice link link String? @db.Text // @deprecated plan String? @db.VarChar(20) // @deprecated recurring String? @db.VarChar(20) @@index([userId]) @@map("user_invoices") } model UserStripeCustomer { userId String @id @map("user_id") @db.VarChar stripeCustomerId String @unique @map("stripe_customer_id") @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_stripe_customers") } model Subscription { id Int @id @default(autoincrement()) @db.Integer targetId String @map("target_id") @db.VarChar plan String @db.VarChar(20) // yearly/monthly/lifetime recurring String @db.VarChar(20) // onetime subscription or anything else variant String? @db.VarChar(20) quantity Int @default(1) @db.Integer // subscription.id, null for lifetime payment or one time payment subscription stripeSubscriptionId String? @unique @map("stripe_subscription_id") // stripe schedule id stripeScheduleId String? @map("stripe_schedule_id") @db.VarChar // subscription.status, active/past_due/canceled/unpaid... status String @db.VarChar(20) // subscription.current_period_start start DateTime @map("start") @db.Timestamptz(3) // subscription.current_period_end, null for lifetime payment end DateTime? @map("end") @db.Timestamptz(3) // subscription.billing_cycle_anchor nextBillAt DateTime? @map("next_bill_at") @db.Timestamptz(3) // subscription.canceled_at canceledAt DateTime? @map("canceled_at") @db.Timestamptz(3) // subscription.trial_start trialStart DateTime? @map("trial_start") @db.Timestamptz(3) // subscription.trial_end trialEnd DateTime? @map("trial_end") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) @@unique([targetId, plan]) @@map("subscriptions") } model Invoice { stripeInvoiceId String @id @map("stripe_invoice_id") targetId String @map("target_id") @db.VarChar currency String @db.VarChar(3) // CNY 12.50 stored as 1250 amount Int @db.Integer status String @db.VarChar(20) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) // billing reason reason String? @db.VarChar lastPaymentError String? @map("last_payment_error") @db.Text // stripe hosted invoice link link String? @db.Text @@index([targetId]) @@map("invoices") } // Blob table only exists for fast non-data queries. // like, total size of blobs in a workspace, or blob list for sync service. // it should only be a map of metadata of blobs stored anywhere else model Blob { workspaceId String @map("workspace_id") @db.VarChar key String @db.VarChar size Int @db.Integer mime String @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@id([workspaceId, key]) @@map("blobs") }