generator client { provider = "prisma-client-js" output = "../../../node_modules/.prisma/client" binaryTargets = ["native", "debian-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] previewFeatures = ["metrics", "relationJoins", "nativeDistinct", "postgresqlExtensions"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") extensions = [pgvector(map: "vector")] } 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) disabled Boolean @default(false) features UserFeature[] userStripeCustomer UserStripeCustomer? workspaces WorkspaceUserRole[] // Invite others to join the workspace WorkspaceInvitations WorkspaceUserRole[] @relation("inviter") docPermissions WorkspaceDocUserRole[] connectedAccounts ConnectedAccount[] calendarAccounts CalendarAccount[] sessions UserSession[] aiSessions AiSession[] appConfigs AppConfig[] userSnapshots UserSnapshot[] createdSnapshot Snapshot[] @relation("createdSnapshot") updatedSnapshot Snapshot[] @relation("updatedSnapshot") createdUpdate Update[] @relation("createdUpdate") createdHistory SnapshotHistory[] @relation("createdHistory") createdAiJobs AiJobs[] @relation("createdAiJobs") // receive notifications notifications Notification[] @relation("user_notifications") settings UserSettings? comments Comment[] replies Reply[] commentAttachments CommentAttachment[] @relation("createdCommentAttachments") AccessToken AccessToken[] workspaceCalendars WorkspaceCalendar[] @@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[] @@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 MagicLinkOtp { id String @id @default(uuid()) @db.VarChar email String @unique @db.Text otpHash String @map("otp_hash") @db.VarChar token String @db.Text clientNonce String? @map("client_nonce") @db.Text attempts Int @default(0) 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) @@index([expiresAt]) @@map("magic_link_otps") } model Workspace { // NOTE: manually set this column type to identity in migration file sid Int @unique @default(autoincrement()) 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") enableSharing Boolean @default(true) @map("enable_sharing") enableUrlPreview Boolean @default(false) @map("enable_url_preview") enableDocEmbedding Boolean @default(true) @map("enable_doc_embedding") name String? @db.VarChar avatarKey String? @map("avatar_key") @db.VarChar indexed Boolean @default(false) lastCheckEmbeddings DateTime @default("1970-01-01T00:00:00-00:00") @map("last_check_embeddings") @db.Timestamptz(3) features WorkspaceFeature[] docs WorkspaceDoc[] permissions WorkspaceUserRole[] docPermissions WorkspaceDocUserRole[] blobs Blob[] ignoredDocs AiWorkspaceIgnoredDocs[] embedFiles AiWorkspaceFiles[] comments Comment[] commentAttachments CommentAttachment[] workspaceCalendars WorkspaceCalendar[] workspaceAdminStats WorkspaceAdminStats[] workspaceAdminStatsDirties WorkspaceAdminStatsDirty[] @@index([lastCheckEmbeddings]) @@index([createdAt]) @@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 business logic. model WorkspaceDoc { workspaceId String @map("workspace_id") @db.VarChar docId String @map("page_id") @db.VarChar public Boolean @default(false) // Workspace user's default role in this page, default is `Manager` defaultRole Int @default(30) @db.SmallInt // Page/Edgeless mode Int @default(0) @db.SmallInt // Whether the doc is blocked blocked Boolean @default(false) title String? @db.VarChar summary String? @db.VarChar publishedAt DateTime? @map("published_at") @db.Timestamptz(3) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@id([workspaceId, docId]) @@index([workspaceId, public]) @@map("workspace_pages") } enum WorkspaceMemberStatus { /// Wait for the invitee to accept the invitation Pending /// Wait for administrators to review and accept the link invitation UnderReview /// Temporary state for team workspace. There is some time gap between invitation and bill payed AllocatingSeat /// Insufficient seat for user becoming active workspace member NeedMoreSeat /// Activate workspace member Accepted /// @deprecated NeedMoreSeatAndReview } enum WorkspaceMemberSource { /// Invited by email Email /// Invited by link Link } model WorkspaceUserRole { id String @id @default(uuid()) @db.VarChar workspaceId String @map("workspace_id") @db.VarChar userId String @map("user_id") @db.VarChar // Workspace Role, Owner/Admin/Collaborator/External type Int @db.SmallInt /// the invite status of the workspace member status WorkspaceMemberStatus @default(Pending) source WorkspaceMemberSource @default(Email) inviterId String? @map("inviter_id") @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) 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) inviter User? @relation(name: "inviter", fields: [inviterId], references: [id], onDelete: SetNull) @@unique([workspaceId, userId]) // optimize for querying user's workspace permissions @@index([workspaceId, type, status]) @@index(userId) @@map("workspace_user_permissions") } model WorkspaceDocUserRole { workspaceId String @map("workspace_id") @db.VarChar docId String @map("page_id") @db.VarChar userId String @map("user_id") @db.VarChar // External/Reader/Editor/Manager/Owner type Int @db.SmallInt 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) @@id([workspaceId, docId, userId]) @@map("workspace_page_user_permissions") } model UserFeature { id Int @id @default(autoincrement()) userId String @map("user_id") @db.VarChar // it should be typed as `optional` in the codebase, but we would keep all values exists during data migration. // so it's safe to assert it a non-null value. name String @default("") @map("name") @db.VarChar // a little redundant, but fast the queries type Int @default(0) @map("type") @db.Integer reason String @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) expiredAt DateTime? @map("expired_at") @db.Timestamptz(3) activated Boolean @default(false) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([name]) @@map("user_features") } model WorkspaceFeature { id Int @id @default(autoincrement()) workspaceId String @map("workspace_id") @db.VarChar // it should be typed as `optional` in the codebase, but we would keep all values exists during data migration. // so it's safe to assert it a non-null value. name String @default("") @map("name") @db.VarChar // a little redundant, but fast the queries type Int @default(0) @map("type") @db.Integer /// overrides for the default feature configs configs Json @default("{}") @db.Json reason String @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) activated Boolean @default(false) expiredAt DateTime? @map("expired_at") @db.Timestamptz(3) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@index([workspaceId]) @@index([name]) // Partial index on (workspace_id, name) where activated = true is created via SQL migration @@index([workspaceId, name, activated]) @@map("workspace_features") } model WorkspaceAdminStats { workspaceId String @id @map("workspace_id") @db.VarChar snapshotCount BigInt @default(0) @map("snapshot_count") @db.BigInt snapshotSize BigInt @default(0) @map("snapshot_size") @db.BigInt blobCount BigInt @default(0) @map("blob_count") @db.BigInt blobSize BigInt @default(0) @map("blob_size") @db.BigInt memberCount BigInt @default(0) @map("member_count") @db.BigInt publicPageCount BigInt @default(0) @map("public_page_count") @db.BigInt features String[] @default([]) @db.Text updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@index([snapshotSize(sort: Desc)]) @@index([blobCount(sort: Desc)]) @@index([blobSize(sort: Desc)]) @@index([snapshotCount(sort: Desc)]) @@index([memberCount(sort: Desc)]) @@index([publicPageCount(sort: Desc)]) @@map("workspace_admin_stats") } model WorkspaceAdminStatsDirty { workspaceId String @id @map("workspace_id") @db.VarChar updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@index([updatedAt]) @@map("workspace_admin_stats_dirty") } // 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 // persisted size of blob to avoid summing over bytea size BigInt? @map("size") @db.BigInt 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) // 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) embedding AiWorkspaceEmbedding[] @@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 history 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 creator record if creator's account is deleted createdByUser User? @relation(name: "createdUpdate", fields: [createdBy], references: [id], onDelete: SetNull) @@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 creator 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 optionalModels String[] @default([]) @map("optional_models") @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 streamObjects Json? @db.Json 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) promptAction String? @default("") @map("prompt_action") @db.VarChar(32) pinned Boolean @default(false) title String? @db.VarChar // 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) updatedAt DateTime @default(now()) @updatedAt @map("updated_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[] context AiContext[] //NOTE: // unrecorded index: // @@index([userId, workspaceId]) where pinned = true and deleted_at is null // @@index([userId, workspaceId, docId]) where prompt_action is null and parent_session_id is null and doc_id is not null and deleted_at is null // since prisma does not support partial indexes, those indexes are only exists in migration files. @@index([promptName]) @@index([userId]) @@index([userId, workspaceId, docId]) @@map("ai_sessions_metadata") } model AiContext { id String @id @default(uuid()) @db.VarChar sessionId String @map("session_id") @db.VarChar config Json @db.Json createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) embeddings AiContextEmbedding[] session AiSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) @@map("ai_contexts") } model AiContextEmbedding { id String @id @default(uuid()) @db.VarChar contextId String @map("context_id") @db.VarChar fileId String @map("file_id") @db.VarChar // a file can be divided into multiple chunks and embedded separately. chunk Int @db.Integer content String @db.VarChar embedding Unsupported("vector(1024)") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) context AiContext @relation(fields: [contextId], references: [id], onDelete: Cascade) @@unique([contextId, fileId, chunk]) @@index([embedding], map: "ai_context_embeddings_idx") @@map("ai_context_embeddings") } model AiWorkspaceEmbedding { workspaceId String @map("workspace_id") @db.VarChar docId String @map("doc_id") @db.VarChar // a doc can be divided into multiple chunks and embedded separately. chunk Int @db.Integer content String @db.VarChar embedding Unsupported("vector(1024)") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) // workspace level search not available for non-cloud workspaces // so we can match this record with the snapshot one by one snapshot Snapshot @relation(fields: [workspaceId, docId], references: [workspaceId, id], onDelete: Cascade) @@id([workspaceId, docId, chunk]) @@index([embedding], map: "ai_workspace_embeddings_idx") @@map("ai_workspace_embeddings") } model AiWorkspaceIgnoredDocs { workspaceId String @map("workspace_id") @db.VarChar docId String @map("doc_id") @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@id([workspaceId, docId]) @@map("ai_workspace_ignored_docs") } model AiWorkspaceFiles { workspaceId String @map("workspace_id") @db.VarChar fileId String @map("file_id") @db.VarChar blobId String @default("") @map("blob_id") @db.VarChar fileName String @map("file_name") @db.VarChar mimeType String @map("mime_type") @db.VarChar size Int @db.Integer createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) embeddings AiWorkspaceFileEmbedding[] @@id([workspaceId, fileId]) @@map("ai_workspace_files") } model AiWorkspaceFileEmbedding { workspaceId String @map("workspace_id") @db.VarChar fileId String @map("file_id") @db.VarChar // a file can be divided into multiple chunks and embedded separately. chunk Int @db.Integer content String @db.VarChar embedding Unsupported("vector(1024)") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) file AiWorkspaceFiles @relation(fields: [workspaceId, fileId], references: [workspaceId, fileId], onDelete: Cascade) @@id([workspaceId, fileId, chunk]) @@index([embedding], map: "ai_workspace_file_embeddings_idx") @@map("ai_workspace_file_embeddings") } model AiWorkspaceBlobEmbedding { workspaceId String @map("workspace_id") @db.VarChar blobId String @map("blob_id") @db.VarChar // a file can be divided into multiple chunks and embedded separately. chunk Int @db.Integer content String @db.VarChar embedding Unsupported("vector(1024)") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) blob Blob @relation(fields: [workspaceId, blobId], references: [workspaceId, key], onDelete: Cascade) @@id([workspaceId, blobId, chunk]) @@index([embedding], map: "ai_workspace_blob_embeddings_idx") @@map("ai_workspace_blob_embeddings") } enum AiJobStatus { pending running finished claimed failed } enum AiJobType { transcription } model AiJobs { id String @id @default(uuid()) @db.VarChar workspaceId String @map("workspace_id") @db.VarChar blobId String @map("blob_id") @db.VarChar createdBy String? @map("created_by") @db.VarChar // job type, like "transcription" type AiJobType status AiJobStatus @default(pending) // job result payload Json @db.Json startedAt DateTime @default(now()) @map("started_at") @db.Timestamptz(3) finishedAt DateTime? @map("finished_at") @db.Timestamptz(3) // will delete creator record if creator's account is deleted createdByUser User? @relation(name: "createdAiJobs", fields: [createdBy], references: [id], onDelete: SetNull) @@unique([createdBy, workspaceId, blobId]) @@map("ai_jobs") } model DataMigration { id String @id @default(uuid()) @db.VarChar name String @unique @db.VarChar startedAt DateTime @default(now()) @map("started_at") @db.Timestamptz(3) finishedAt DateTime? @map("finished_at") @db.Timestamptz(3) @@map("_data_migrations") } model AppConfig { id String @id @db.VarChar value Json @db.JsonB createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) lastUpdatedBy String? @map("last_updated_by") @db.VarChar lastUpdatedByUser User? @relation(fields: [lastUpdatedBy], references: [id], onDelete: SetNull) @@map("app_configs") } 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 provider: stripe or revenuecat provider Provider @default(stripe) // iap store for revenuecat subscriptions iapStore IapStore? @map("iap_store") // revenuecat entitlement name like "Pro" / "AI" rcEntitlement String? @map("rc_entitlement") @db.VarChar // revenuecat product id like "app.affine.pro.Annual" rcProductId String? @map("rc_product_id") @db.VarChar // external reference, appstore originalTransactionId or play purchaseToken rcExternalRef String? @map("rc_external_ref") @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") } enum Provider { stripe revenuecat } enum IapStore { app_store play_store } 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 // whether the onetime subscription has been redeemed onetimeSubscriptionRedeemed Boolean @default(false) @map("onetime_subscription_redeemed") @@index([targetId]) @@map("invoices") } model License { key String @id @map("key") @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) revealedAt DateTime? @map("revealed_at") @db.Timestamptz(3) installedAt DateTime? @map("installed_at") @db.Timestamptz(3) validateKey String? @map("validate_key") @db.VarChar @@map("licenses") } model InstalledLicense { key String @id @map("key") @db.VarChar workspaceId String @unique @map("workspace_id") @db.VarChar quantity Int @default(1) @db.Integer recurring String @db.VarChar variant String? @db.VarChar installedAt DateTime @default(now()) @map("installed_at") @db.Timestamptz(3) validateKey String @map("validate_key") @db.VarChar validatedAt DateTime @map("validated_at") @db.Timestamptz(3) expiredAt DateTime? @map("expired_at") @db.Timestamptz(3) license Bytes? @db.ByteA @@map("installed_licenses") } enum BlobStatus { pending completed } // 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 status BlobStatus @default(completed) uploadId String? @map("upload_id") @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) AiWorkspaceBlobEmbedding AiWorkspaceBlobEmbedding[] @@id([workspaceId, key]) @@index([workspaceId, status, deletedAt]) @@map("blobs") } enum NotificationType { Mention Invitation InvitationAccepted InvitationBlocked InvitationRejected InvitationReviewRequest InvitationReviewApproved InvitationReviewDeclined Comment CommentMention } enum NotificationLevel { // Makes a sound and appears as a heads-up notification High // Makes a sound Default // Makes no sound Low Min None } model Notification { id String @id @default(uuid()) @db.VarChar userId String @map("user_id") @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3) level NotificationLevel read Boolean @default(false) type NotificationType body Json @db.JsonB user User @relation(name: "user_notifications", fields: [userId], references: [id], onDelete: Cascade) // for user notifications list, including read and unread, ordered by createdAt @@index([userId, createdAt, read]) @@map("notifications") } model UserSettings { userId String @id @map("user_id") @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) payload Json @db.JsonB user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_settings") } model Comment { // NOTE: manually set this column type to identity in migration file sid Int @unique @default(autoincrement()) @db.Integer id String @id @default(uuid()) @db.VarChar workspaceId String @map("workspace_id") @db.VarChar docId String @map("doc_id") @db.VarChar userId String @map("user_id") @db.VarChar content Json @db.JsonB createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3) // whether the comment is resolved resolved Boolean @default(false) @map("resolved") user User @relation(fields: [userId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) replies Reply[] @@index([workspaceId, docId, sid]) @@index([workspaceId, docId, updatedAt]) @@index([userId]) @@map("comments") } model Reply { // NOTE: manually set this column type to identity in migration file sid Int @unique @default(autoincrement()) @db.Integer id String @id @default(uuid()) @db.VarChar userId String @map("user_id") @db.VarChar commentId String @map("comment_id") @db.VarChar // query new replies by workspaceId and docId workspaceId String @map("workspace_id") @db.VarChar docId String @map("doc_id") @db.VarChar content Json @db.JsonB createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3) user User @relation(fields: [userId], references: [id], onDelete: Cascade) comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) @@index([commentId, sid]) @@index([workspaceId, docId, updatedAt]) @@index([userId]) @@map("replies") } model CommentAttachment { // NOTE: manually set this column type to identity in migration file sid Int @unique @default(autoincrement()) workspaceId String @map("workspace_id") @db.VarChar docId String @map("doc_id") @db.VarChar key String @db.VarChar size Int @db.Integer mime String @db.VarChar name String @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) createdBy String? @map("created_by") @db.VarChar workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) // will delete creator record if creator's account is deleted createdByUser User? @relation(name: "createdCommentAttachments", fields: [createdBy], references: [id], onDelete: SetNull) @@id([workspaceId, docId, key]) @@map("comment_attachments") } model AccessToken { id String @id @default(uuid()) @db.VarChar name String @db.VarChar token String @unique @db.VarChar userId String @map("user_id") @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) expiresAt DateTime? @map("expires_at") @db.Timestamptz(3) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@map("access_tokens") } model CalendarAccount { 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 displayName String? @map("display_name") @db.VarChar email String? @db.VarChar providerPresetId String? @map("provider_preset_id") @db.VarChar serverUrl String? @map("server_url") @db.VarChar principalUrl String? @map("principal_url") @db.VarChar calendarHomeUrl String? @map("calendar_home_url") @db.VarChar username String? @db.VarChar authType String? @map("auth_type") @db.VarChar accessToken String? @map("access_token") @db.Text refreshToken String? @map("refresh_token") @db.Text expiresAt DateTime? @map("expires_at") @db.Timestamptz(3) scope String? @db.Text status String @default("active") @db.VarChar lastError String? @map("last_error") @db.Text refreshIntervalMinutes Int @default(30) @map("refresh_interval_minutes") 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) subscriptions CalendarSubscription[] @@unique([userId, provider, providerAccountId]) @@index([userId]) @@index([provider, providerAccountId]) @@map("calendar_accounts") } model CalendarSubscription { id String @id @default(uuid()) @db.VarChar accountId String @map("account_id") @db.VarChar provider String @db.VarChar externalCalendarId String @map("external_calendar_id") @db.VarChar displayName String? @map("display_name") @db.VarChar timezone String? @db.VarChar color String? @db.VarChar enabled Boolean @default(true) syncToken String? @map("sync_token") @db.Text lastSyncAt DateTime? @map("last_sync_at") @db.Timestamptz(3) customChannelId String? @map("custom_channel_id") @db.VarChar customResourceId String? @map("custom_resource_id") @db.VarChar channelExpiration DateTime? @map("channel_expiration") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) account CalendarAccount @relation(fields: [accountId], references: [id], onDelete: Cascade) workspaceItems WorkspaceCalendarItem[] events CalendarEvent[] @@unique([accountId, externalCalendarId]) @@index([accountId]) @@index([provider, externalCalendarId]) @@map("calendar_subscriptions") } model WorkspaceCalendar { id String @id @default(uuid()) @db.VarChar workspaceId String @map("workspace_id") @db.VarChar createdByUserId String @map("created_by_user_id") @db.VarChar displayNameOverride String? @map("display_name_override") @db.VarChar colorOverride String? @map("color_override") @db.VarChar enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) createdByUser User @relation(fields: [createdByUserId], references: [id], onDelete: Cascade) items WorkspaceCalendarItem[] @@index([workspaceId]) @@map("workspace_calendars") } model WorkspaceCalendarItem { id String @id @default(uuid()) @db.VarChar workspaceCalendarId String @map("workspace_calendar_id") @db.VarChar subscriptionId String @map("subscription_id") @db.VarChar sortOrder Int? @map("sort_order") colorOverride String? @map("color_override") @db.VarChar enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) workspaceCalendar WorkspaceCalendar @relation(fields: [workspaceCalendarId], references: [id], onDelete: Cascade) subscription CalendarSubscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) @@unique([workspaceCalendarId, subscriptionId]) @@index([subscriptionId]) @@map("workspace_calendar_items") } model CalendarEvent { id String @id @default(uuid()) @db.VarChar subscriptionId String @map("subscription_id") @db.VarChar externalEventId String @map("external_event_id") @db.VarChar recurrenceId String? @map("recurrence_id") @db.VarChar etag String? @db.VarChar status String? @db.VarChar title String? @db.VarChar description String? @db.Text location String? @db.VarChar startAtUtc DateTime @map("start_at_utc") @db.Timestamptz(3) endAtUtc DateTime @map("end_at_utc") @db.Timestamptz(3) originalTimezone String? @map("original_timezone") @db.VarChar allDay Boolean @default(false) @map("all_day") providerUpdatedAt DateTime? @map("provider_updated_at") @db.Timestamptz(3) raw Json @db.JsonB createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) subscription CalendarSubscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) instances CalendarEventInstance[] @@unique([subscriptionId, externalEventId, recurrenceId]) @@index([subscriptionId, startAtUtc]) @@index([subscriptionId, endAtUtc]) @@map("calendar_events") } model CalendarEventInstance { id String @id @default(uuid()) @db.VarChar calendarEventId String @map("calendar_event_id") @db.VarChar recurrenceId String @map("recurrence_id") @db.VarChar startAtUtc DateTime @map("start_at_utc") @db.Timestamptz(3) endAtUtc DateTime @map("end_at_utc") @db.Timestamptz(3) originalTimezone String? @map("original_timezone") @db.VarChar allDay Boolean @default(false) @map("all_day") providerUpdatedAt DateTime? @map("provider_updated_at") @db.Timestamptz(3) raw Json @db.JsonB createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) calendarEvent CalendarEvent @relation(fields: [calendarEventId], references: [id], onDelete: Cascade) @@unique([calendarEventId, recurrenceId]) @@index([calendarEventId, startAtUtc]) @@map("calendar_event_instances") }