generator client { provider = "prisma-client-js" previewFeatures = ["driverAdapters", "relationJoins"] } datasource db { provider = "postgresql" url = env("STACK_DATABASE_CONNECTION_STRING") directUrl = env("STACK_DIRECT_DATABASE_CONNECTION_STRING") } model Project { // Note that the project with ID `internal` is handled as a special case. All other project IDs are UUIDs. id String @id createdAt DateTime @default(now()) updatedAt DateTime @updatedAt displayName String description String @default("") isProductionMode Boolean ownerTeamId String? @db.Uuid logoUrl String? fullLogoUrl String? projectConfigOverride Json? stripeAccountId String? apiKeySets ApiKeySet[] projectUsers ProjectUser[] provisionedProject ProvisionedProject? tenancies Tenancy[] environmentConfigOverrides EnvironmentConfigOverride[] } model Tenancy { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt projectId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) branchId String // If organizationId is NULL, hasNoOrganization must be TRUE. If organizationId is not NULL, hasNoOrganization must be NULL. organizationId String? @db.Uuid hasNoOrganization BooleanTrue? @@unique([projectId, branchId, organizationId]) @@unique([projectId, branchId, hasNoOrganization]) } model EnvironmentConfigOverride { projectId String branchId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt config Json project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) @@id([projectId, branchId]) } model Team { tenancyId String @db.Uuid teamId String @default(uuid()) @db.Uuid // Team IDs must be unique across all organizations (but not necessarily across all branches). // To model this in the DB, we add two columns that are always equal to tenancy.projectId and tenancy.branchId. mirroredProjectId String mirroredBranchId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt displayName String clientMetadata Json? clientReadOnlyMetadata Json? serverMetadata Json? profileImageUrl String? teamMembers TeamMember[] projectApiKey ProjectApiKey[] @@id([tenancyId, teamId]) @@unique([mirroredProjectId, mirroredBranchId, teamId]) } // This is used for fields that are boolean but only the true value is part of a unique constraint. // For example if you want to allow only one selected team per user, you can make an optional field with this type and add a unique constraint. // Only the true value is considered for the unique constraint, the null value is not. enum BooleanTrue { TRUE } model TeamMember { tenancyId String @db.Uuid projectUserId String @db.Uuid teamId String @db.Uuid // This will override the displayName of the user in this team. displayName String? profileImageUrl String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) team Team @relation(fields: [tenancyId, teamId], references: [tenancyId, teamId], onDelete: Cascade) isSelected BooleanTrue? teamMemberDirectPermissions TeamMemberDirectPermission[] @@id([tenancyId, projectUserId, teamId]) @@unique([tenancyId, projectUserId, isSelected]) } model ProjectUserDirectPermission { id String @id @default(uuid()) @db.Uuid tenancyId String @db.Uuid projectUserId String @db.Uuid permissionId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@unique([tenancyId, projectUserId, permissionId]) } model TeamMemberDirectPermission { id String @id @default(uuid()) @db.Uuid tenancyId String @db.Uuid projectUserId String @db.Uuid teamId String @db.Uuid permissionId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt teamMember TeamMember @relation(fields: [tenancyId, projectUserId, teamId], references: [tenancyId, projectUserId, teamId], onDelete: Cascade) @@unique([tenancyId, projectUserId, teamId, permissionId]) } model ProjectUser { tenancyId String @db.Uuid projectUserId String @default(uuid()) @db.Uuid // User IDs must be unique across all organizations (but not necessarily across all branches). // To model this in the DB, we add two columns that are always equal to tenancy.projectId and tenancy.branchId. mirroredProjectId String mirroredBranchId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt displayName String? serverMetadata Json? clientReadOnlyMetadata Json? clientMetadata Json? profileImageUrl String? requiresTotpMfa Boolean @default(false) totpSecret Bytes? isAnonymous Boolean @default(false) projectUserOAuthAccounts ProjectUserOAuthAccount[] teamMembers TeamMember[] contactChannels ContactChannel[] authMethods AuthMethod[] // some backlinks for the unique constraints on some auth methods passwordAuthMethod PasswordAuthMethod[] passkeyAuthMethod PasskeyAuthMethod[] otpAuthMethod OtpAuthMethod[] oauthAuthMethod OAuthAuthMethod[] SentEmail SentEmail[] projectApiKey ProjectApiKey[] directPermissions ProjectUserDirectPermission[] Project Project? @relation(fields: [projectId], references: [id]) projectId String? userNotificationPreference UserNotificationPreference[] @@id([tenancyId, projectUserId]) @@unique([mirroredProjectId, mirroredBranchId, projectUserId]) // indices for sorting and filtering @@index([tenancyId, displayName(sort: Asc)], name: "ProjectUser_displayName_asc") @@index([tenancyId, displayName(sort: Desc)], name: "ProjectUser_displayName_desc") @@index([tenancyId, createdAt(sort: Asc)], name: "ProjectUser_createdAt_asc") @@index([tenancyId, createdAt(sort: Desc)], name: "ProjectUser_createdAt_desc") } // This should be renamed to "OAuthAccount" as it is not always bound to a user // When ever a user goes through the OAuth flow and gets an account ID from the OAuth provider, we store that here. model ProjectUserOAuthAccount { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid projectUserId String? @db.Uuid configOAuthProviderId String providerAccountId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // This is used for the user to distinguish between multiple accounts from the same provider. // we might want to add more user info here later email String? // Before the OAuth account is connected to a user (for example, in the link oauth process), the projectUser is null. projectUser ProjectUser? @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) oauthTokens OAuthToken[] oauthAccessToken OAuthAccessToken[] // if allowSignIn is true, oauthAuthMethod must be set oauthAuthMethod OAuthAuthMethod? allowConnectedAccounts Boolean @default(true) allowSignIn Boolean @default(true) @@id([tenancyId, id]) @@unique([tenancyId, configOAuthProviderId, projectUserId, providerAccountId]) @@index([tenancyId, projectUserId]) } enum ContactChannelType { EMAIL // PHONE } model ContactChannel { tenancyId String @db.Uuid projectUserId String @db.Uuid id String @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt type ContactChannelType isPrimary BooleanTrue? usedForAuth BooleanTrue? isVerified Boolean value String projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, projectUserId, id]) // each user has at most one primary contact channel of each type @@unique([tenancyId, projectUserId, type, isPrimary]) // value must be unique per user per type @@unique([tenancyId, projectUserId, type, value]) // only one contact channel per project with the same value and type can be used for auth @@unique([tenancyId, type, value, usedForAuth]) } model AuthMethod { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid projectUserId String @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // exactly one of the xyzAuthMethods should be set otpAuthMethod OtpAuthMethod? passwordAuthMethod PasswordAuthMethod? passkeyAuthMethod PasskeyAuthMethod? oauthAuthMethod OAuthAuthMethod? projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, id]) @@index([tenancyId, projectUserId]) } model OtpAuthMethod { tenancyId String @db.Uuid authMethodId String @db.Uuid projectUserId String @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade) projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, authMethodId]) // a user can only have one OTP auth method @@unique([tenancyId, projectUserId]) } model PasswordAuthMethod { tenancyId String @db.Uuid authMethodId String @db.Uuid projectUserId String @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt passwordHash String authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade) projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, authMethodId]) // a user can only have one password auth method @@unique([tenancyId, projectUserId]) } model PasskeyAuthMethod { tenancyId String @db.Uuid authMethodId String @db.Uuid projectUserId String @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt credentialId String publicKey String userHandle String transports String[] credentialDeviceType String counter Int authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade) projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, authMethodId]) // a user can only have one password auth method @@unique([tenancyId, projectUserId]) } // This connects to projectUserOauthAccount, which might be shared between auth method and connected account. model OAuthAuthMethod { tenancyId String @db.Uuid authMethodId String @db.Uuid configOAuthProviderId String providerAccountId String projectUserId String @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade) oauthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, configOAuthProviderId, projectUserId, providerAccountId], references: [tenancyId, configOAuthProviderId, projectUserId, providerAccountId]) projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, authMethodId]) @@unique([tenancyId, configOAuthProviderId, providerAccountId]) @@unique([tenancyId, projectUserId, configOAuthProviderId]) @@unique([tenancyId, configOAuthProviderId, projectUserId, providerAccountId]) } enum StandardOAuthProviderType { GITHUB FACEBOOK GOOGLE MICROSOFT SPOTIFY DISCORD GITLAB BITBUCKET LINKEDIN APPLE X TWITCH } model OAuthToken { id String @id @default(uuid()) @db.Uuid tenancyId String @db.Uuid oauthAccountId String @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt projectUserOAuthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, oauthAccountId], references: [tenancyId, id], onDelete: Cascade) refreshToken String scopes String[] isValid Boolean @default(true) } model OAuthAccessToken { id String @id @default(uuid()) @db.Uuid tenancyId String @db.Uuid oauthAccountId String @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt projectUserOAuthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, oauthAccountId], references: [tenancyId, id], onDelete: Cascade) accessToken String scopes String[] expiresAt DateTime isValid Boolean @default(true) } model OAuthOuterInfo { id String @id @default(uuid()) @db.Uuid info Json innerState String @unique expiresAt DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model ProjectUserRefreshToken { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid projectUserId String @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt refreshToken String @unique expiresAt DateTime? isImpersonation Boolean @default(false) @@id([tenancyId, id]) } model ProjectUserAuthorizationCode { tenancyId String @db.Uuid projectUserId String @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt authorizationCode String @unique redirectUri String expiresAt DateTime codeChallenge String codeChallengeMethod String newUser Boolean afterCallbackRedirectUrl String? @@id([tenancyId, authorizationCode]) } model VerificationCode { projectId String branchId String id String @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt type VerificationCodeType code String expiresAt DateTime usedAt DateTime? redirectUrl String? method Json @default("null") data Json attemptCount Int @default(0) @@id([projectId, branchId, id]) @@unique([projectId, branchId, code]) @@index([data(ops: JsonbPathOps)], type: Gin) } enum VerificationCodeType { ONE_TIME_PASSWORD PASSWORD_RESET CONTACT_CHANNEL_VERIFICATION TEAM_INVITATION MFA_ATTEMPT PASSKEY_REGISTRATION_CHALLENGE PASSKEY_AUTHENTICATION_CHALLENGE INTEGRATION_PROJECT_TRANSFER PURCHASE_URL } //#region API keys // Internal API keys model ApiKeySet { projectId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) id String @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt description String expiresAt DateTime manuallyRevokedAt DateTime? publishableClientKey String? @unique secretServerKey String? @unique superSecretAdminKey String? @unique @@id([projectId, id]) } //#endregion model ProjectApiKey { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid secretApiKey String @unique // Validity and revocation createdAt DateTime @default(now()) updatedAt DateTime @updatedAt expiresAt DateTime? manuallyRevokedAt DateTime? description String isPublic Boolean // exactly one of [teamId] or [projectUserId] must be set teamId String? @db.Uuid projectUserId String? @db.Uuid projectUser ProjectUser? @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) team Team? @relation(fields: [tenancyId, teamId], references: [tenancyId, teamId], onDelete: Cascade) @@id([tenancyId, id]) } enum EmailTemplateType { EMAIL_VERIFICATION PASSWORD_RESET MAGIC_LINK TEAM_INVITATION SIGN_IN_INVITATION } model EmailTemplate { projectId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt content Json type EmailTemplateType subject String @@id([projectId, type]) } //#region IdP model IdPAccountToCdfcResultMapping { idpId String id String idpAccountId String @unique @db.Uuid cdfcResult Json @@id([idpId, id]) } model ProjectWrapperCodes { idpId String id String @default(uuid()) @db.Uuid interactionUid String authorizationCode String @unique cdfcResult Json @@id([idpId, id]) } model IdPAdapterData { idpId String model String id String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt payload Json expiresAt DateTime @@id([idpId, model, id]) @@index([payload(ops: JsonbPathOps)], type: Gin) @@index([expiresAt]) } //#endregion model ProvisionedProject { projectId String @id project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt clientId String } //#region Events model Event { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // if isWide == false, then eventEndedAt is always equal to eventStartedAt isWide Boolean eventStartedAt DateTime eventEndedAt DateTime // TODO: add event_type, and at least one of either system_event_type or event_type is always set systemEventTypeIds String[] data Json // ============================== BEGIN END USER PROPERTIES ============================== // Below are properties describing the end user that caused this event to be logged // This is different from a request IP. See: apps/backend/src/lib/end-users.tsx // Note that the IP may have been spoofed, unless isEndUserIpInfoGuessTrusted is true endUserIpInfoGuessId String? @db.Uuid endUserIpInfoGuess EventIpInfo? @relation("EventIpInfo", fields: [endUserIpInfoGuessId], references: [id]) // If true, then endUserIpInfoGuess is not spoofed (might still be behind VPNs/proxies). If false, then the values may be spoofed. isEndUserIpInfoGuessTrusted Boolean @default(false) // =============================== END END USER PROPERTIES =============================== @@index([data(ops: JsonbPathOps)], type: Gin) } // An IP address that was seen in an event. Use the location fields instead of refetching the location from the ip, as the real-world geoip data may have changed since the event was logged. model EventIpInfo { id String @id @default(uuid()) @db.Uuid ip String countryCode String? regionCode String? cityName String? latitude Float? longitude Float? tzIdentifier String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt events Event[] @relation("EventIpInfo") } //#endregion model SentEmail { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid userId String? @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt senderConfig Json to String[] subject String html String? text String? error Json? user ProjectUser? @relation(fields: [tenancyId, userId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, id]) } model EmailDraft { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid displayName String themeMode DraftThemeMode @default(PROJECT_DEFAULT) themeId String? tsxSource String sentAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([tenancyId, id]) } enum DraftThemeMode { PROJECT_DEFAULT NONE CUSTOM } model CliAuthAttempt { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid pollingCode String @unique loginCode String @unique refreshToken String? expiresAt DateTime usedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([tenancyId, id]) } model UserNotificationPreference { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid projectUserId String @db.Uuid notificationCategoryId String @db.Uuid enabled Boolean projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, id]) @@unique([tenancyId, projectUserId, notificationCategoryId]) } model ThreadMessage { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid threadId String @db.Uuid content Json createdAt DateTime @default(now()) @@id([tenancyId, id]) } enum CustomerType { USER TEAM CUSTOM } enum SubscriptionStatus { active trialing canceled paused incomplete incomplete_expired past_due unpaid } enum PurchaseCreationSource { PURCHASE_PAGE TEST_MODE } model Subscription { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid customerId String customerType CustomerType offerId String? priceId String? offer Json quantity Int @default(1) stripeSubscriptionId String? status SubscriptionStatus currentPeriodEnd DateTime currentPeriodStart DateTime cancelAtPeriodEnd Boolean creationSource PurchaseCreationSource createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([tenancyId, id]) @@unique([tenancyId, stripeSubscriptionId]) } model ItemQuantityChange { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid customerId String customerType CustomerType itemId String quantity Int description String? expiresAt DateTime? createdAt DateTime @default(now()) @@id([tenancyId, id]) @@index([tenancyId, customerId, expiresAt]) } model OneTimePurchase { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid customerId String customerType CustomerType offerId String? priceId String? offer Json quantity Int stripePaymentIntentId String? createdAt DateTime @default(now()) creationSource PurchaseCreationSource @@id([tenancyId, id]) @@unique([tenancyId, stripePaymentIntentId]) } model DataVaultEntry { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid storeId String hashedKey String encrypted Json // Contains { edkBase64, ciphertextBase64 } from encryptWithKms() createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([tenancyId, id]) @@unique([tenancyId, storeId, hashedKey]) @@index([tenancyId, storeId]) } model WorkflowTriggerToken { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid tokenHash String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt expiresAt DateTime @@id([tenancyId, id]) @@unique([tenancyId, tokenHash]) } model WorkflowTrigger { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid executionId String @db.Uuid triggerData Json // the following fields determine the state of the trigger: // - scheduledAt && !compiledWorkflowId && !output && !error: the trigger is scheduled to be executed // - !scheduledAt && !compiledWorkflowId: the trigger was scheduled, but its workflow subsequently deleted. The trigger never ran // - !scheduledAt && compiledWorkflowId && !output && !error: the trigger is currently executing // - !scheduledAt && compiledWorkflowId && output && !error: the trigger has successfully completed execution // - !scheduledAt && compiledWorkflowId && !output && error: the trigger has failed execution // All other combinations are invalid. scheduledAt DateTime? output Json? error Json? compiledWorkflowId String? @db.Uuid compiledWorkflow CompiledWorkflow? @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id]) @@id([tenancyId, id]) } model WorkflowExecution { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid workflowId String triggerIds String[] triggers WorkflowTrigger[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([tenancyId, id]) } model CurrentlyCompilingWorkflow { tenancyId String @db.Uuid workflowId String compilationVersion Int sourceHash String startedCompilingAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([tenancyId, workflowId, compilationVersion, sourceHash]) } model CompiledWorkflow { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid workflowId String // note: The workflow with this ID may have been edited or deleted in the meantime, so there may be multiple CompiledWorkflows with the same workflowId compilationVersion Int sourceHash String // exactly one of [compiledCode, compileError] must be set compiledCode String? compileError String? compiledAt DateTime @default(now()) registeredTriggers String[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt workflowTriggers WorkflowTrigger[] @@id([tenancyId, id]) @@unique([tenancyId, workflowId, compilationVersion, sourceHash]) }