// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ======================== // Auth // ======================== model User { id String @id @default(cuid()) email String @unique name String passwordHash String role UserRole @default(VIEWER) isActive Boolean @default(true) allServersAccess Boolean @default(true) activeOrganizationId String? lastLogin DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt activeOrganization Organization? @relation("UserActiveOrganization", fields: [activeOrganizationId], references: [id], onDelete: SetNull) organizationMemberships OrganizationMembership[] createdOrganizations Organization[] @relation("OrganizationCreator") apiKeys ApiKey[] auditLogs AuditLog[] settings UserSettings? notificationProviders UserNotificationProvider[] integrations UserIntegration[] gitProviders UserGitProvider[] storageDestinations UserStorageDestination[] serverAssignments UserServerAccess[] sentInvitations UserInvitation[] @relation("InvitationCreator") @@index([activeOrganizationId]) @@map("users") } model Organization { id String @id @default(cuid()) name String slug String @unique logoUrl String? createdById String? createdBy User? @relation("OrganizationCreator", fields: [createdById], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt members OrganizationMembership[] activeUsers User[] @relation("UserActiveOrganization") servers Server[] projects Project[] domains Domain[] apiKeys ApiKey[] invitations UserInvitation[] auditLogs AuditLog[] @@index([createdById]) @@map("organizations") } model OrganizationMembership { id String @id @default(cuid()) organizationId String userId String isDefault Boolean @default(false) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @@unique([organizationId, userId]) @@index([userId]) @@map("organization_memberships") } // ======================== // Projects // ======================== model Project { id String @id @default(cuid()) organizationId String name String slug String description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) environments Environment[] @@unique([organizationId, slug]) @@index([organizationId]) @@map("projects") } model Environment { id String @id @default(cuid()) projectId String serverId String name String slug String kind EnvironmentKind description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) server Server @relation(fields: [serverId], references: [id], onDelete: Restrict) containers Container[] @@unique([projectId, slug]) @@index([projectId]) @@index([projectId, kind]) @@index([serverId]) @@map("environments") } enum EnvironmentKind { PRODUCTION STAGING DEVELOPMENT PREVIEW CUSTOM } model UserSettings { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) panelName String @default("DOKTAINER") panelUrl String @default("http://localhost:3000") timezone String @default("Asia/Jakarta") theme String @default("dark") sessionTimeoutMinutes Int @default(30) twoFactorEnabled Boolean @default(false) twoFactorSecretEnc String? twoFactorPendingSecretEnc String? ipWhitelistEnabled Boolean @default(false) ipWhitelist String[] @default([]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("user_settings") } model UserNotificationProvider { id String @id @default(cuid()) userId String organizationId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) type NotificationProviderType name String enabled Boolean @default(true) actions String[] @default([]) channel String? webhookUrlEnc String? smtpHost String? smtpPort Int @default(587) smtpSecure Boolean @default(false) smtpUsername String? smtpPasswordEnc String? smtpFromEmail String? smtpFromName String? telegramChatId String? telegramBotTokenEnc String? serverUrl String? topic String? userKey String? apiKeyEnc String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId, organizationId, type]) @@index([organizationId]) @@map("user_notification_providers") } model UserIntegration { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) provider IntegrationProviderType connected Boolean @default(false) zoneId String? apiTokenEnc String? accessKeyId String? secretAccessKeyEnc String? region String? bucket String? tokenEnc String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([userId, provider]) @@index([userId]) @@map("user_integrations") } model UserStorageDestination { id String @id @default(cuid()) userId String organizationId String serverId String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) server Server? @relation(fields: [serverId], references: [id], onDelete: SetNull) name String provider S3StorageProvider enabled Boolean @default(true) accessKeyId String secretAccessKeyEnc String? region String? bucket String endpoint String? additionalFlags String[] @default([]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId, organizationId]) @@index([organizationId, serverId]) @@map("user_storage_destinations") } model UserGitProvider { id String @id @default(cuid()) userId String organizationId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) containerDeploymentSources ContainerDeploymentSource[] provider GitProviderType name String enabled Boolean @default(true) appName String appId String? clientId String? clientSecretEnc String? webhookSecretEnc String? appUrl String? installationUrl String? providerUrl String? internalUrl String? accountUsername String? accountEmail String? namespace String? organizationScoped Boolean @default(false) organizationName String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId, organizationId, provider]) @@index([organizationId]) @@map("user_git_providers") } model UserServerAccess { id String @id @default(cuid()) userId String serverId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) server Server @relation(fields: [serverId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @@unique([userId, serverId]) @@index([serverId]) @@map("user_server_access") } model UserInvitation { id String @id @default(cuid()) organizationId String email String name String role UserRole tokenHash String @unique allServersAccess Boolean @default(true) serverIds String[] invitedById String? organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) invitedBy User? @relation("InvitationCreator", fields: [invitedById], references: [id], onDelete: SetNull) expiresAt DateTime acceptedAt DateTime? revokedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([email]) @@index([expiresAt]) @@index([organizationId]) @@map("user_invitations") } enum UserRole { SUPER_ADMIN OPERATOR DEVELOPER VIEWER } enum NotificationProviderType { SLACK TELEGRAM DISCORD LARK TEAMS EMAIL RESEND GOTIFY NTFY MATTERMOST PUSHOVER CUSTOM } enum IntegrationProviderType { CLOUDFLARE AWS_S3 GITHUB GITLAB } enum GitProviderType { GITHUB GITLAB BITBUCKET GITEA } enum S3StorageProvider { AWS_S3 ALIBABA_OSS ARVAN_AOS CEPH CHINA_MOBILE_EOS CLOUDFLARE_R2 DIGITALOCEAN_SPACES DREAMOBJECTS GOOGLE_CLOUD_STORAGE HUAWEI_OBS IBM_COS CUSTOM } model ApiKey { id String @id @default(cuid()) organizationId String name String keyHash String @unique keyPrefix String permissions String[] userId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) lastUsed DateTime? expiresAt DateTime? isActive Boolean @default(true) requestCount Int @default(0) createdAt DateTime @default(now()) @@index([organizationId]) @@map("api_keys") } // ======================== // Servers // ======================== model Server { id String @id @default(cuid()) organizationId String name String ip String sshPort Int @default(22) username String @default("root") authType SshAuthType @default(PASSWORD) sshKeyEnc String? // encrypted private key passwordEnc String? // encrypted password status ServerStatus @default(UNKNOWN) os String? location String? tags String[] lastHealthAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) containers Container[] environments Environment[] networks Network[] storageDestinations UserStorageDestination[] domains Domain[] auditLogs AuditLog[] metrics ServerMetric[] appInstalls AppInstall[] backups Backup[] firewallRulePresets FirewallRulePreset[] userAssignments UserServerAccess[] @@index([organizationId]) @@map("servers") } enum SshAuthType { PASSWORD SSH_KEY } enum ServerStatus { ONLINE OFFLINE WARNING UNKNOWN } model ServerMetric { id String @id @default(cuid()) serverId String server Server @relation(fields: [serverId], references: [id], onDelete: Cascade) cpuPct Float ramPct Float diskPct Float ramUsed BigInt ramTotal BigInt diskUsed BigInt diskTotal BigInt networkRxBps BigInt? networkTxBps BigInt? uptimeSec BigInt recordedAt DateTime @default(now()) @@index([serverId, recordedAt]) @@map("server_metrics") } // ======================== // Containers // ======================== model Container { id String @id @default(cuid()) dockerId String? name String image String status ContainerStatus @default(STOPPED) sourceType ContainerSourceType @default(MANUAL) deployMode ContainerDeployMode? serverId String server Server @relation(fields: [serverId], references: [id], onDelete: Cascade) environmentId String? environment Environment? @relation(fields: [environmentId], references: [id], onDelete: SetNull) domains Domain[] deploymentSource ContainerDeploymentSource? ports Json @default("[]") // [{host: 80, container: 80, protocol: "tcp"}] envVars Json @default("[]") // [{key: "NODE_ENV", value: "production"}] volumes Json @default("[]") labels Json @default("{}") restartPolicy String @default("unless-stopped") cpuUsage String? ramUsage String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([environmentId]) @@index([serverId]) @@map("containers") } model ContainerDeploymentSource { id String @id @default(cuid()) containerId String @unique container Container @relation(fields: [containerId], references: [id], onDelete: Cascade) projectName String? gitProviderId String? gitProvider UserGitProvider? @relation(fields: [gitProviderId], references: [id], onDelete: SetNull) repoUrl String? repoBranch String? repoVisibility RepositoryVisibility? buildType String? buildPath String? startCommand String? portOverride String? publishDirectory String? imageTag String? accessTokenEnc String? composeEnvOverrides Json? projectPath String? composeFilePath String? dockerfilePath String? dockerContextPath String? deploymentPath String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([gitProviderId]) @@map("container_deployment_sources") } model Network { id String @id @default(cuid()) name String driver String @default("bridge") scope String @default("local") subnet String? gateway String? containers Int @default(0) serverId String server Server @relation(fields: [serverId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([serverId, name]) @@index([serverId]) @@map("networks") } model FirewallRulePreset { id String @id @default(cuid()) serverId String server Server @relation(fields: [serverId], references: [id], onDelete: Cascade) rule String action String direction String @default("IN") source String @default("Anywhere") description String @default("") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([serverId]) @@unique([serverId, rule, action, direction, source]) @@map("firewall_rule_presets") } enum ContainerStatus { RUNNING STOPPED STARTING STOPPING ERROR PAUSED } enum ContainerSourceType { APP_INSTALLER MANUAL GIT_CLONE GIT_PROVIDER } enum ContainerDeployMode { IMAGE DOCKERFILE COMPOSE } enum RepositoryVisibility { PUBLIC PRIVATE } // ======================== // Domains & SSL // ======================== model Domain { id String @id @default(cuid()) name String @unique organizationId String domainNameType DomainNameType @default(EXACT) configMode DomainConfigMode @default(ISOLATED) reviewStatus DomainReviewStatus @default(CONFIRMED) isPrimary Boolean @default(false) type DnsType @default(A) value String // IP or CNAME target serverId String? organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) server Server? @relation(fields: [serverId], references: [id]) targetContainerId String? targetContainer Container? @relation(fields: [targetContainerId], references: [id], onDelete: SetNull) targetPort Int? proxy ProxyType @default(NONE) discoverySource DomainDiscoverySource @default(MANUAL) sslEnabled Boolean @default(false) autoRenew Boolean @default(true) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt sslCert SslCert? @@index([organizationId]) @@index([targetContainerId]) @@map("domains") } enum DnsType { A AAAA CNAME MX TXT } enum ProxyType { TRAEFIK NGINX CADDY NONE } enum DomainDiscoverySource { MANUAL NGINX TRAEFIK CADDY CADDY_ADMIN CERTBOT } enum DomainNameType { EXACT WILDCARD } enum DomainConfigMode { SHARED ISOLATED } enum DomainReviewStatus { CONFIRMED NEEDS_REVIEW } model SslCert { id String @id @default(cuid()) domainId String @unique domain Domain @relation(fields: [domainId], references: [id], onDelete: Cascade) issuer String @default("Let's Encrypt") certPem String? // certificate PEM keyPem String? // private key PEM (encrypted) issuedAt DateTime? expiresAt DateTime? status SslStatus @default(PENDING) autoRenew Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("ssl_certs") } enum SslStatus { VALID EXPIRING EXPIRED PENDING FAILED } // ======================== // Audit Log // ======================== model AuditLog { id String @id @default(cuid()) userId String? organizationId String? user User? @relation(fields: [userId], references: [id]) organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull) serverId String? server Server? @relation(fields: [serverId], references: [id]) action String category LogCategory level LogLevel @default(INFO) message String meta Json? createdAt DateTime @default(now()) @@index([createdAt]) @@index([organizationId]) @@index([serverId]) @@map("audit_logs") } enum LogCategory { AUTH SERVER CONTAINER DOMAIN SSL SECURITY SYSTEM TERMINAL } enum LogLevel { DEBUG INFO WARNING ERROR SUCCESS } // ======================== // App Installer // ======================== model AppInstall { id String @id @default(cuid()) appId String // e.g. "wordpress", "nginx" appName String serverId String server Server @relation(fields: [serverId], references: [id], onDelete: Cascade) containerName String? // docker container name port String? // host port used status AppInstallStatus @default(PENDING) error String? installedAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("app_installs") } enum AppInstallStatus { PENDING INSTALLING RUNNING STARTING STOPPING STOPPED PAUSED UNKNOWN FAILED REMOVED } // ======================== // Backups // ======================== model Backup { id String @id @default(cuid()) name String type BackupType databaseEngine DatabaseEngine? serverId String server Server @relation(fields: [serverId], references: [id], onDelete: Cascade) target String @default("Local") sizeMb Float? filePath String? status BackupStatus @default(PENDING) error String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("backups") } enum BackupType { DATABASE VOLUME FULL } enum DatabaseEngine { POSTGRESQL MYSQL MARIADB MONGODB REDIS } enum BackupStatus { PENDING RUNNING COMPLETED FAILED }