generator client { provider = "prisma-client-js" previewFeatures = ["tracing", "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 userCount Int @default(0) apiKeySets ApiKeySet[] projectUsers ProjectUser[] provisionedProject ProvisionedProject? tenancies Tenancy[] verificationCodes VerificationCode[] projectApiKey ProjectApiKey[] environmentConfigOverrides EnvironmentConfigOverride[] } enum OAuthAccountMergeStrategy { LINK_METHOD RAISE_ERROR ALLOW_DUPLICATES } 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? teams Team[] @relation("TenancyTeams") projectUsers ProjectUser[] @relation("TenancyProjectUsers") authMethods AuthMethod[] @relation("TenancyAuthMethods") contactChannels ContactChannel[] @relation("TenancyContactChannels") connectedAccounts ConnectedAccount[] @relation("TenancyConnectedAccounts") SentEmail SentEmail[] cliAuthAttempts CliAuthAttempt[] projectApiKey ProjectApiKey[] @@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 profileImageUrl String? clientMetadata Json? clientReadOnlyMetadata Json? serverMetadata Json? tenancy Tenancy @relation("TenancyTeams", fields: [tenancyId], references: [id], onDelete: Cascade) 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? // This will override the profileImageUrl of the user in this team. 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 profileImageUrl String? displayName String? serverMetadata Json? clientReadOnlyMetadata Json? clientMetadata Json? requiresTotpMfa Boolean @default(false) totpSecret Bytes? isAnonymous Boolean @default(false) tenancy Tenancy @relation("TenancyProjectUsers", fields: [tenancyId], references: [id], onDelete: Cascade) project Project @relation(fields: [mirroredProjectId], references: [id], onDelete: Cascade) projectUserRefreshTokens ProjectUserRefreshToken[] projectUserAuthorizationCodes ProjectUserAuthorizationCode[] projectUserOAuthAccounts ProjectUserOAuthAccount[] teamMembers TeamMember[] contactChannels ContactChannel[] authMethods AuthMethod[] connectedAccounts ConnectedAccount[] // some backlinks for the unique constraints on some auth methods passwordAuthMethod PasswordAuthMethod[] passkeyAuthMethod PasskeyAuthMethod[] otpAuthMethod OtpAuthMethod[] oauthAuthMethod OAuthAuthMethod[] SentEmail SentEmail[] projectApiKey ProjectApiKey[] directPermissions ProjectUserDirectPermission[] @@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 { 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 use (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[] // At lease one of the authMethod or connectedAccount should be set. connectedAccount ConnectedAccount? oauthAuthMethod OAuthAuthMethod? @@id([tenancyId, configOAuthProviderId, 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 tenancy Tenancy @relation("TenancyContactChannels", fields: [tenancyId], references: [id], onDelete: Cascade) 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 ConnectedAccount { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid projectUserId String @db.Uuid configOAuthProviderId String providerAccountId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt oauthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, configOAuthProviderId, providerAccountId], references: [tenancyId, configOAuthProviderId, providerAccountId]) tenancy Tenancy @relation("TenancyConnectedAccounts", fields: [tenancyId], references: [id], onDelete: Cascade) projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, id]) @@unique([tenancyId, configOAuthProviderId, providerAccountId]) } // Both the connected account and auth methods can use this configuration. enum ProxiedOAuthProviderType { GITHUB GOOGLE MICROSOFT SPOTIFY } 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? tenancy Tenancy @relation("TenancyAuthMethods", fields: [tenancyId], references: [id], onDelete: Cascade) 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, providerAccountId], references: [tenancyId, configOAuthProviderId, providerAccountId]) projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, authMethodId]) @@unique([tenancyId, configOAuthProviderId, providerAccountId]) } enum StandardOAuthProviderType { GITHUB FACEBOOK GOOGLE MICROSOFT SPOTIFY DISCORD GITLAB BITBUCKET LINKEDIN APPLE X } model OAuthToken { id String @id @default(uuid()) @db.Uuid tenancyId String @db.Uuid configOAuthProviderId String providerAccountId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt projectUserOAuthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, configOAuthProviderId, providerAccountId], references: [tenancyId, configOAuthProviderId, providerAccountId], onDelete: Cascade) refreshToken String scopes String[] } model OAuthAccessToken { id String @id @default(uuid()) @db.Uuid tenancyId String @db.Uuid configOAuthProviderId String providerAccountId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt projectUserOAuthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, configOAuthProviderId, providerAccountId], references: [tenancyId, configOAuthProviderId, providerAccountId], onDelete: Cascade) accessToken String scopes String[] expiresAt DateTime } 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) projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@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? projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, authorizationCode]) } model VerificationCode { projectId String branchId String id String @default(uuid()) @db.Uuid project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) 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 } //#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]) } model ProjectApiKey { projectId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) 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 tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade) 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 } model EmailTemplate { projectId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt content Json type EmailTemplateType subject String @@id([projectId, type]) } //#endregion //#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? tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade) user ProjectUser? @relation(fields: [tenancyId, userId], references: [tenancyId, projectUserId], onDelete: Cascade) @@id([tenancyId, id]) } model CliAuthAttempt { tenancyId String @db.Uuid tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade) 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]) }