generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ── Users ── model User { id String @id @default(cuid()) email String @unique name String? passwordHash String role Role @default(USER) provider String? externalId String? version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt sessions Session[] auditLogs AuditLog[] ownedProjects Project[] groupMemberships GroupMember[] mcpTokens McpToken[] @@index([email]) } enum Role { USER ADMIN } // ── Sessions ── model Session { id String @id @default(cuid()) token String @unique userId String expiresAt DateTime createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([token]) @@index([userId]) @@index([expiresAt]) } // ── MCP Servers ── model McpServer { id String @id @default(cuid()) name String @unique description String @default("") packageName String? runtime String? dockerImage String? transport Transport @default(STDIO) repositoryUrl String? externalUrl String? command Json? containerPort Int? replicas Int @default(1) env Json @default("[]") healthCheck Json? version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt templateName String? templateVersion String? instances McpInstance[] projects ProjectServer[] @@index([name]) } enum Transport { STDIO SSE STREAMABLE_HTTP } // ── MCP Templates ── model McpTemplate { id String @id @default(cuid()) name String @unique version String @default("1.0.0") description String @default("") packageName String? runtime String? dockerImage String? transport Transport @default(STDIO) repositoryUrl String? externalUrl String? command Json? containerPort Int? replicas Int @default(1) env Json @default("[]") healthCheck Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([name]) } // ── Secret Backends ── // // Pluggable storage for Secret.data. Default is `plaintext` (data stored in // Secret.data JSON). Other drivers (e.g. `openbao`) store only a reference in // Secret.externalRef and fetch actual values from the external system at read // time. A `plaintext` row is seeded on first startup so the system always has // a viable backend; additional backends are user-managed via // `mcpctl create secretbackend`. model SecretBackend { id String @id @default(cuid()) name String @unique type String // plaintext | openbao | (future: vault, aws-sm, ...) config Json @default("{}") // type-specific: url, mount, namespace, tokenSecretRef isDefault Boolean @default(false) // exactly one row has isDefault=true description String @default("") version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt secrets Secret[] @@index([name]) @@index([isDefault]) } // ── Secrets ── model Secret { id String @id @default(cuid()) name String @unique backendId String // FK to SecretBackend — dispatches read/write data Json @default("{}") // populated by plaintext backend only externalRef String @default("") // populated by non-plaintext backends (e.g. "mount/path#v3") version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt backend SecretBackend @relation(fields: [backendId], references: [id]) llms Llm[] @@index([name]) @@index([backendId]) } // ── LLMs ── // // Server-managed LLM providers. Clients (agent, HTTP-mode mcplocal) send // OpenAI-format requests to `mcpd /api/v1/llms/:name/infer` — mcpd attaches the // provider API key server-side so credentials never leave the cluster. // Credentials are stored by reference: `apiKeySecret` points at a Secret, and // `apiKeySecretKey` names the key within that secret's data. model Llm { id String @id @default(cuid()) name String @unique type String // anthropic | openai | deepseek | vllm | ollama | gemini-cli model String // e.g. claude-3-5-sonnet-20241022 url String @default("") // endpoint (empty for provider default) tier String @default("fast") // fast | heavy description String @default("") apiKeySecretId String? // FK to Secret apiKeySecretKey String? // key inside the Secret's data extraConfig Json @default("{}") // per-type extras version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt apiKeySecret Secret? @relation(fields: [apiKeySecretId], references: [id], onDelete: SetNull) @@index([name]) @@index([tier]) @@index([apiKeySecretId]) } // ── Groups ── model Group { id String @id @default(cuid()) name String @unique description String @default("") version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt members GroupMember[] @@index([name]) } model GroupMember { id String @id @default(cuid()) groupId String userId String createdAt DateTime @default(now()) group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([groupId, userId]) @@index([groupId]) @@index([userId]) } // ── RBAC Definitions ── model RbacDefinition { id String @id @default(cuid()) name String @unique subjects Json @default("[]") roleBindings Json @default("[]") version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([name]) } // ── Projects ── model Project { id String @id @default(cuid()) name String @unique description String @default("") prompt String @default("") proxyModel String @default("") gated Boolean @default(true) llmProvider String? llmModel String? serverOverrides Json? ownerId String version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) servers ProjectServer[] prompts Prompt[] promptRequests PromptRequest[] mcpTokens McpToken[] @@index([name]) @@index([ownerId]) } model ProjectServer { id String @id @default(cuid()) projectId String serverId String createdAt DateTime @default(now()) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) server McpServer @relation(fields: [serverId], references: [id], onDelete: Cascade) @@unique([projectId, serverId]) } // ── MCP Tokens (bearer credentials for HTTP-mode mcplocal) ── // // Raw value format: `mcpctl_pat_<32 base62 chars>`. The raw value is shown // exactly once at create time; only the SHA-256 hash is persisted. Tokens are // scoped to exactly one project — they're only valid at // `/projects//mcp`. Creator's RBAC is the ceiling; the service // rejects bindings that exceed what the creator themselves can do. model McpToken { id String @id @default(cuid()) name String projectId String tokenHash String @unique tokenPrefix String ownerId String description String @default("") createdAt DateTime @default(now()) expiresAt DateTime? lastUsedAt DateTime? revokedAt DateTime? project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) @@unique([name, projectId]) @@index([tokenHash]) @@index([projectId]) @@index([ownerId]) } // ── MCP Instances (running containers) ── model McpInstance { id String @id @default(cuid()) serverId String containerId String? status InstanceStatus @default(STOPPED) port Int? metadata Json @default("{}") healthStatus String? lastHealthCheck DateTime? events Json @default("[]") version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt server McpServer @relation(fields: [serverId], references: [id], onDelete: Cascade) @@index([serverId]) @@index([status]) } enum InstanceStatus { STARTING RUNNING STOPPING STOPPED ERROR } // ── Prompts (approved content resources) ── model Prompt { id String @id @default(cuid()) name String content String @db.Text projectId String? priority Int @default(5) summary String? @db.Text chapters Json? linkTarget String? version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) @@unique([name, projectId]) @@index([projectId]) } // ── Prompt Requests (pending proposals from LLM sessions) ── model PromptRequest { id String @id @default(cuid()) name String content String @db.Text projectId String? priority Int @default(5) createdBySession String? createdByUserId String? createdAt DateTime @default(now()) project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) @@unique([name, projectId]) @@index([projectId]) @@index([createdBySession]) } // ── Audit Events (pipeline/gate/tool trace from mcplocal) ── model AuditEvent { id String @id @default(cuid()) timestamp DateTime sessionId String projectName String eventKind String source String verified Boolean @default(false) serverName String? correlationId String? parentEventId String? userName String? tokenName String? tokenSha String? payload Json createdAt DateTime @default(now()) @@index([sessionId]) @@index([projectName]) @@index([correlationId]) @@index([timestamp]) @@index([eventKind]) @@index([userName]) @@index([tokenSha]) } // ── Backup Pending Queue ── model BackupPending { id String @id @default(cuid()) resourceKind String resourceName String action String // 'create' | 'update' | 'delete' userName String yamlContent String? @db.Text createdAt DateTime @default(now()) @@index([createdAt]) } // ── Audit Logs ── model AuditLog { id String @id @default(cuid()) userId String action String resource String resourceId String? details Json @default("{}") createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([action]) @@index([resource]) @@index([createdAt]) }