stack/apps/backend/prisma/schema.prisma
Zai Shi d9e2dae4c6
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Docker Emulator Test / docker (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Test / docker (push) Has been cancelled
Runs E2E API Tests / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Preview Docs / run (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
Config DB migration step 2 (#629)
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
Co-authored-by: moritz <moritsch@student.ethz.ch>
2025-04-29 14:52:45 -07:00

741 lines
23 KiB
Plaintext

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[]
neonProvisionedProject NeonProvisionedProject?
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
NEON_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
//#region Neon integration
model NeonProvisionedProject {
projectId String @id
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
neonClientId String
}
//#endregion
//#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])
}