webapps/console/prisma/schema.prisma
Rama newjitsu · réplica de lectura del repositorio original.
generator zod {
provider = "zod-prisma"
output = "./schema"
relationModel = false
modelCase = "PascalCase"
modelSuffix = "DbModel"
useDecimalJs = true
prismaJsonNullability = true
}
generator client {
provider = "prisma-client-js"
extendedIndexes = true
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model EmailLog {
id String @id @default(cuid())
time DateTime @default(now())
email Json
error String?
previewUrl String?
messageId String?
status String
}
model UserPreferences {
id String @id @default(cuid())
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
userId String
user UserProfile @relation(fields: [userId], references: [id])
//If preferences relates to workspace,
workspaceId String?
workspace Workspace? @relation(fields: [workspaceId], references: [id])
//empty or 'server-only'. Server-only means that preferences can't be changed from JS. NOT USED so far
scope String?
preferences Json
}
model UserProfile {
id String @id @default(cuid())
name String
email String
admin Boolean? @default(false)
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
loginProvider String
externalUsername String?
externalId String
tokens UserApiToken[]
workspaceAccess WorkspaceAccess[]
userPreferences UserPreferences[]
workspaceUserProperties WorkspaceUserProperties[]
password UserPassword?
@@unique([loginProvider, externalId])
@@index(externalId)
}
model UserPassword {
id String @id @default(cuid())
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
userId String @unique
user UserProfile @relation(fields: [userId], references: [id])
hash String
changeAtNextLogin Boolean @default(false)
}
model Workspace {
id String @id @default(cuid())
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
name String
deleted Boolean @default(false)
slug String?
workspaceAccess WorkspaceAccess[]
workspaceOptions WorkspaceOptions[]
configurationObject ConfigurationObject[]
configurationObjectLink ConfigurationObjectLink[]
preferences UserPreferences[]
workspaceUserProperties WorkspaceUserProperties[]
featuresEnabled String[] @default([])
profileBuilders ProfileBuilder[]
oidcLoginGroups OidcLoginGroup[]
@@unique(slug)
@@index(slug)
@@index(updatedAt(sort: Desc))
}
//additional properties for workspace
model WorkspaceOptions {
id String @id @default(cuid())
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
namespace String
value Json
@@index([workspaceId, namespace])
}
enum WorkspaceRole {
owner
editor
analyst
}
model WorkspaceAccess {
workspaceId String
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
userId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
user UserProfile @relation(fields: [userId], references: [id])
role WorkspaceRole @default(owner)
@@id([userId, workspaceId])
}
model WorkspaceUserProperties {
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
userId String
user UserProfile @relation(fields: [userId], references: [id])
lastUsed DateTime @default(now()) /// @zod.custom(z.coerce.date())
@@id([workspaceId, userId])
}
model UserApiToken {
id String @id @default(cuid())
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
lastUsed DateTime? /// @zod.custom(z.coerce.date())
hint String
hash String
userId String
user UserProfile @relation(fields: [userId], references: [id])
type String?
name String?
expiresAt DateTime? /// @zod.custom(z.coerce.date())
// For MCP-issued tokens: oauthClientId is set and this row IS the refresh
// token. MCP-ness is inferred from oauthClientId IS NOT NULL (we don't set
// type="mcp"). For non-MCP keys (UI/CLI/api) oauthClientId is null.
oauthClientId String?
oauthClient OAuthClient? @relation(fields: [oauthClientId], references: [id])
accessTokens OAuthAccessToken[]
}
model OAuthClient {
id String @id @default(cuid()) // == OAuth client_id
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
clientSecretHash String // hashed via juava.createHash
name String // from DCR client_name; shown on /user
redirectUris String[] // whitelisted at DCR time
// Schema is 1:N (a client could in principle hold many refresh tokens).
// We enforce 1:1 in code:
// 1. At /oauth/token (authorize success): delete any existing
// UserApiToken rows for this client (cascading to their access
// tokens) before creating the new one.
// 2. At DELETE /api/user/keys: when a token with oauthClientId is
// deleted, also delete the OAuthClient row and its access tokens
// in the same transaction.
// To migrate to 1:N later: drop step (1) and replace step (2) with a
// separate "revoke connection" action. No schema change required.
tokens UserApiToken[]
}
// Generic key-value store used by the MCP server (and any future feature
// that wants ephemeral or atomic key/value semantics). Single flat
// namespace; callers separate concerns via key prefix conventions
// (`oauth:code:...`, `mcp:event:...`). See lib/server/kv/.
model KvStoreEntry {
key String @id
value Json
expiresAt DateTime? @map("expires_at") /// @zod.custom(z.coerce.date())
@@index([expiresAt])
// Prefix scans (`segment1:segment2:*`) need text_pattern_ops to use the
// index — the default text ops only help equality and ordering. With this
// ops class, `WHERE key LIKE 'prefix%'` becomes an index range scan.
@@index([key(ops: raw("text_pattern_ops"))], map: "kv_key_pattern_idx")
@@map("kv")
}
model OAuthAccessToken {
id String @id @default(cuid()) // bearer keyId
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
lastUsed DateTime? /// @zod.custom(z.coerce.date())
hash String // juava.createHash(plaintext); no hint (never shown)
expiresAt DateTime /// @zod.custom(z.coerce.date())
refreshTokenId String // user is inferred via refreshToken.userId
refreshToken UserApiToken @relation(fields: [refreshTokenId], references: [id])
@@index([expiresAt])
@@index([refreshTokenId])
}
model InvitationToken {
id String @id @default(cuid())
workspaceId String
email String
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
token String
usedBy String?
role WorkspaceRole @default(owner)
}
model ConfigurationObject {
id String @id @default(cuid())
deleted Boolean? @default(false)
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
type String
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
config Json?
fromLinks ConfigurationObjectLink[] @relation("from")
toLinks ConfigurationObjectLink[] @relation("to")
ProfileBuilder ProfileBuilder[]
ProfileBuilderFunction ProfileBuilderFunction[]
@@index(updatedAt(sort: Desc))
}
model ConfigurationObjectLink {
id String @id @default(cuid())
deleted Boolean? @default(false)
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
type String? @default("push")
data Json?
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
fromId String
from ConfigurationObject @relation(fields: [fromId], references: [id], name: "to")
toId String
to ConfigurationObject @relation(fields: [toId], references: [id], name: "from")
@@index(updatedAt(sort: Desc))
@@index(workspaceId)
}
model ConnectorPackage {
id String @id @default(cuid())
packageId String
packageType String @default("airbyte")
meta Json?
logoSvg Bytes?
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
}
model AuditLog {
id String @id @default(cuid())
timestamp DateTime @default(now()) /// @zod.custom(z.coerce.date())
type String
severity String? @default("info")
userId String?
workspaceId String?
objectId String?
changes Json?
authType String?
tokenId String?
ip String?
requestHeaders Json?
@@index(timestamp)
@@index(type)
@@index([workspaceId, timestamp])
}
model GlobalProps {
id String @id @default(cuid())
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
name String
value Json?
}
model source_spec {
package String
version String
specs Json?
timestamp DateTime @default(now()) @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
error String?
@@id([package, version])
}
model source_check {
package String
version String
key String @id
status String
description String?
timestamp DateTime @default(now()) @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
}
model source_catalog {
package String
version String
key String
catalog Json?
timestamp DateTime @default(now()) @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
status String
description String?
@@id([package, version, key])
}
// Reverse ETL tables share the config database schema and its update workflow.
// Prisma defines their schema/types; the Node runner uses pg for atomic transactions.
// Worker coordination belongs to Kubernetes leases and syncctl, not these tables.
enum reverse_sync_mode {
upsert
mirror
}
enum reverse_sync_extraction {
cursor
full
}
// One durable lifecycle record per logical run: immutable config/target
// binding, phase, in-progress provider state, sequence boundaries and the durable artifact head.
// Status refresh workers reuse the original source_task and append to its logs.
// source_state holds committed checkpoints; this row also tracks unfinished work.
model reverse_sync_control {
workspace_id String
sync_id String
run_id String
// Monotonic per-sync admission order, including runs still processing remotely.
run_order BigInt @default(0)
// Initial extraction/uploads are durably submitted; newer runs may now start.
detached Boolean @default(false)
revision String
target_hash String
phase String @default("new")
mode reverse_sync_mode
extraction reverse_sync_extraction
// Immutable object-storage manifest reference. New runs always insert a pointer;
// null identifies legacy per-row state requiring an explicit operator reset.
artifact_head Bytes?
base_sequence BigInt @default(0)
next_sequence BigInt @default(0)
checkpoint_sequence BigInt @default(0)
finish_sequence BigInt?
store Bytes?
provider_state Bytes?
finish_result Bytes?
committed_generation String?
@@id([workspace_id, sync_id, run_id])
@@index([sync_id])
@@unique([workspace_id, sync_id, run_order])
}
// Persistent exclusive claim of a destination audience by a mirror sync. Prevents
// other mirror/upsert syncs from managing that same target across runs/workspaces.
// This is audience ownership, not a worker lease; it survives completion/failure.
model reverse_sync_target_owner {
target_hash String @id
workspace_id String
sync_id String
@@unique([workspace_id, sync_id])
}
// Shared committed checkpoint storage for connector syncs and Reverse ETL.
// Reverse ETL uses the _REVERSE_ETL_ stream for scoped cursor/sequence, provider
// store and generation state; detailed unfinished delivery stays in its journal.
model source_state {
sync_id String
stream String
state Json
timestamp DateTime @default(now()) @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
@@id([sync_id, stream])
}
// Shared execution-attempt status and heartbeat for syncctl-managed syncs.
// Reverse ETL uses one task ID per logical run, including status-refresh workers.
// PENDING means submitted/processing; COMPLETE means accepted, not matched audience size.
model source_task {
sync_id String
task_id String @id
package String
version String
started_at DateTime @default(now()) @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
updated_at DateTime @default(now()) @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
started_by Json?
status String
description String?
metrics Json?
error String?
@@index(sync_id)
@@index(started_at)
// Backs syncctl's CleanupTaskLogs retention sweep
// (bulker/sync-sidecar/db/db.go). Lets the per-sync window function be
// index-only — without this the hourly DELETE scans the whole table.
@@index([sync_id, started_at(sort: Desc)])
}
// Diagnostic messages attached to a sync/task execution attempt. Reverse ETL writes
// core-owned lifecycle messages here; delivery receipts and recovery evidence live
// in durable object-storage batches/receipts instead of being inferred from logs.
model task_log {
id String @id @default(uuid()) @db.Uuid
level String
logger String
message String
sync_id String
task_id String
timestamp DateTime @default(now()) @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
@@index(sync_id)
@@index(task_id)
@@index(timestamp)
}
model ProfileBuilder {
id String @id @default(cuid())
name String
deleted Boolean? @default(false)
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
intermediateStorageCredentials Json
destinationId String?
destination ConfigurationObject? @relation(fields: [destinationId], references: [id])
connectionOptions Json?
functions ProfileBuilderFunction[]
version Int @default(0)
}
model ProfileBuilderFunction {
id String @id @default(cuid())
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
profileBuilderId String
profileBuilder ProfileBuilder @relation(fields: [profileBuilderId], references: [id])
functionId String
function ConfigurationObject @relation(fields: [functionId], references: [id])
}
model ProfileBuilderState {
profileBuilderId String
profileBuilderVersion Int
instanceIndex Int
totalInstances Int
startedAt DateTime
updatedAt DateTime
lastTimestamp DateTime?
processedUsers Int
totalUsers Int
errorUsers Int
speed Float
@@id([profileBuilderId, profileBuilderVersion, totalInstances, instanceIndex])
}
model ProfileBuilderState2 {
profileBuilderId String
updatedAt DateTime
fullRebuildInfo Json?
queuesInfo Json?
metrics Json?
@@id([profileBuilderId])
}
model StatusChange {
id BigInt @id @default(autoincrement()) @db.BigInt
workspaceId String
actorId String
type String
tableName String
timestamp DateTime @default(now()) @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
status String
startedAt DateTime
counts Int @default(0)
description String?
queueSize Int @default(0)
@@index([actorId, type, tableName])
@@index(timestamp)
@@index(startedAt)
}
model NotificationState {
workspaceId String
actorId String
type String
tableName String
channelId String
flappingSince DateTime? @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
lastNotification DateTime @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
statusChangeId BigInt
error String?
@@id([channelId, actorId, type, tableName])
}
model Notification {
id String @id @default(uuid()) @db.Uuid
workspaceId String
actorId String
type String
tableName String
channelId String
statusChangeId BigInt
timestamp DateTime @default(now()) @db.Timestamptz(3) /// @zod.custom(z.coerce.date())
status String
error String?
@@index([actorId, type, tableName])
@@index(timestamp)
}
// OIDC Provider configuration for custom authentication
model OidcProvider {
id String @id @default(cuid())
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
// Display name for the provider
name String
// OIDC configuration
issuer String // OIDC issuer URL (e.g., https://auth.company.com)
clientId String
clientSecret String // Should be encrypted in production
// OIDC endpoints - if autoDiscovery is true, these will be fetched from /.well-known/openid-configuration
authorizationEndpoint String? // maps to authorization_endpoint
tokenEndpoint String? // maps to token_endpoint
userinfoEndpoint String? // maps to userinfo_endpoint
jwksUri String? // maps to jwks_uri
introspectionEndpoint String? // maps to introspection_endpoint
// OAuth scopes to request
scopes String[] @default(["openid", "profile", "email", "offline_access"])
// OAuth/OIDC parameters
audience String? // The audience parameter for the authorization request (defaults to clientId)
prompt String? // The prompt parameter for the authorization request (e.g., "login", "consent")
// Claims mapping - which OIDC claims to use for user properties
usernameClaim String @default("email") // Claim to use as username
emailClaim String @default("email") // Claim to use for email
nameClaim String @default("name") // Claim to use for display name
groupClaim String @default("groups") // Which claim contains user's groups
enabled Boolean @default(true)
autoDiscovery Boolean @default(true) // Whether to use OIDC discovery endpoint
oidcLoginGroups OidcLoginGroup[]
@@unique([issuer, clientId])
}
// Links OIDC providers to workspaces with access control rules
model OidcLoginGroup {
id String @id @default(cuid())
createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
oidcProviderId String
oidcProvider OidcProvider @relation(fields: [oidcProviderId], references: [id])
// Group-based access control
groupValue String? // Required group value (e.g., "engineering", "admins")
// If true, all authenticated users from this OIDC provider can access the workspace
// If false, only users with matching groupValue in their groupClaim can access
allowAllUsers Boolean @default(false)
@@unique([workspaceId, oidcProviderId])
@@index(workspaceId)
@@index(oidcProviderId)
}
model reprocessing_jobs {
id String @id @default(cuid()) @db.VarChar(64)
config Json
status String @db.VarChar(32)
created_at DateTime @db.Timestamptz(6)
started_at DateTime? @db.Timestamptz(6)
completed_at DateTime? @db.Timestamptz(6)
k8s_job_name String? @db.VarChar(255)
total_files Int? @default(0)
total_workers Int? @default(0)
error String?
reprocessing_workers reprocessing_workers[]
@@index([created_at(sort: Desc)])
}
model reprocessing_workers {
job_id String @db.VarChar(64)
worker_index Int
status String @db.VarChar(32)
started_at DateTime? @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
completed_at DateTime? @db.Timestamptz(6)
current_file String?
current_line BigInt? @default(0)
current_ts String?
assigned_files Int? @default(0)
processed_files Int? @default(0)
total_lines BigInt? @default(0)
success_count BigInt? @default(0)
error_count BigInt? @default(0)
skipped_count BigInt? @default(0)
total_bytes BigInt? @default(0)
processed_bytes BigInt? @default(0)
error String?
job reprocessing_jobs @relation(fields: [job_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@id([job_id, worker_index])
@@index([job_id])
@@index([job_id, status], map: "reprocessing_workers_status_idx")
}
model FunctionsServer {
workspaceId String
class String // 'free' | 'dedicated' | 'premium'
deploymentId String
connections String[]
emptyConnections String[]
profileBuilders String[]
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime? @db.Timestamptz(3)
@@id([workspaceId, class])
}
//model OauthSecrets {
// id String @id @default(cuid())
// createdAt DateTime @default(now()) /// @zod.custom(z.coerce.date())
// updatedAt DateTime @default(now()) @updatedAt /// @zod.custom(z.coerce.date())
// nangoIntegrationId String
// secrets Json?
//}
model RateLimitCounter {
bucketKey String
windowStart DateTime @db.Timestamptz(3)
count Int @default(0)
expiresAt DateTime @db.Timestamptz(3)
@@id([bucketKey, windowStart])
@@index([expiresAt])
}