// Omniloq — Schéma Prisma (v2 — phase1-hardening) // Revue ligne par ligne intégrée : // - FK réelles partout (ChannelIdentity.tenant, Job.conversation, activeRevision, // triggerMessage, message.run, replyTo, toolCall.mcpServer, assignee, secret) // - AgentTool unique (agentId, mcpServerId, toolName) // - AgentRun enrichi (finishReason, totalTokens, usageRaw, costMicrousd, toolPolicySnapshot) // - ToolCall : idempotencyKey unique + champs confirmation dédiés // - Lease : version (fencing token) // - Job : lockExpiresAt (recovery) // - timestamps → Timestamptz(3), tout UTC // - enums sur les états internes stables generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum TenantMemberRole { OWNER ADMIN DEVELOPER OPERATOR VIEWER } enum ChannelType { whatsapp telegram web } enum ConversationMode { AI HUMAN HYBRID } enum ConversationStatus { open closed archived } enum MessageDirection { inbound outbound } enum SenderType { contact agent human system } enum MessageStatus { received queued sent delivered read failed } enum RunStatus { pending running completed failed cancelled } enum ToolCallStatus { pending executing success error awaiting_confirmation } enum JobStatus { pending locked done failed } model Tenant { id String @id @default(cuid()) name String slug String @unique createdAt DateTime @default(now()) @db.Timestamptz(3) updatedAt DateTime @updatedAt @db.Timestamptz(3) members TenantMember[] contacts Contact[] channelAccounts ChannelAccount[] channelIdentities ChannelIdentity[] agents Agent[] mcpServers McpServer[] secrets Secret[] auditLogs AuditLog[] conversations Conversation[] jobs Job[] } model User { id String @id @default(cuid()) email String @unique passwordHash String? createdAt DateTime @default(now()) @db.Timestamptz(3) memberships TenantMember[] } model TenantMember { id String @id @default(cuid()) tenantId String userId String role TenantMemberRole @default(OPERATOR) tenant Tenant @relation(fields: [tenantId], references: [id]) user User @relation(fields: [userId], references: [id]) assignedConversations Conversation[] @relation("ConversationAssignee") @@unique([tenantId, userId]) } model Contact { id String @id @default(cuid()) tenantId String createdAt DateTime @default(now()) @db.Timestamptz(3) tenant Tenant @relation(fields: [tenantId], references: [id]) identities ChannelIdentity[] } model ChannelAccount { id String @id @default(cuid()) tenantId String channelType ChannelType provider String? // meta | bird | gupshup externalAccountId String? label String? status String @default("DISCONNECTED") config Json? secretId String? createdAt DateTime @default(now()) @db.Timestamptz(3) tenant Tenant @relation(fields: [tenantId], references: [id]) secret Secret? @relation("ChannelAccountSecret", fields: [secretId], references: [id]) identities ChannelIdentity[] @@index([tenantId]) } model ChannelIdentity { id String @id @default(cuid()) tenantId String contactId String? channelType ChannelType channelAccountId String externalUserId String createdAt DateTime @default(now()) @db.Timestamptz(3) tenant Tenant @relation(fields: [tenantId], references: [id]) contact Contact? @relation(fields: [contactId], references: [id]) channelAccount ChannelAccount @relation(fields: [channelAccountId], references: [id]) conversations Conversation[] @@unique([channelType, channelAccountId, externalUserId]) @@index([tenantId]) @@index([contactId]) } model Agent { id String @id @default(cuid()) tenantId String name String activeRevisionId String? createdAt DateTime @default(now()) @db.Timestamptz(3) tenant Tenant @relation(fields: [tenantId], references: [id]) activeRevision AgentRevision? @relation("AgentActiveRevision", fields: [activeRevisionId], references: [id]) revisions AgentRevision[] @relation("AgentRevisions") tools AgentTool[] conversations Conversation[] variables Variable[] @@index([tenantId]) } model AgentRevision { id String @id @default(cuid()) agentId String systemPrompt String provider String model String providerOptions Json? maxSteps Int @default(8) temperature Float @default(0.3) toolsetVersion String createdAt DateTime @default(now()) @db.Timestamptz(3) createdBy String? agent Agent @relation("AgentRevisions", fields: [agentId], references: [id]) activeForAgents Agent[] @relation("AgentActiveRevision") runs AgentRun[] @@index([agentId]) } model AgentTool { id String @id @default(cuid()) agentId String mcpServerId String toolName String enabled Boolean @default(true) requiresConfirmation Boolean @default(false) riskLevel String @default("low") timeoutMs Int @default(15000) maxCallsPerRun Int @default(3) idempotencyRequired Boolean @default(false) allowedRoles Json? agent Agent @relation(fields: [agentId], references: [id]) mcpServer McpServer @relation(fields: [mcpServerId], references: [id]) @@unique([agentId, mcpServerId, toolName]) } model McpServer { id String @id @default(cuid()) tenantId String name String transport String @default("http") // http | sse (stdio = INTERNAL/TRUSTED uniquement, jamais exposé aux tenants) url String? headersTemplate Json? // ex: {"Authorization": "Bearer {{secret:MCP_TOKEN}}"} enabled Boolean @default(true) tenant Tenant @relation(fields: [tenantId], references: [id]) agentTools AgentTool[] toolCalls ToolCall[] } model Conversation { id String @id @default(cuid()) tenantId String agentId String channelIdentityId String externalThreadId String? mode ConversationMode @default(AI) status ConversationStatus @default(open) assignedToMemberId String? handoffReason String? handoffAt DateTime? @db.Timestamptz(3) resumePolicy String? startedAt DateTime @default(now()) @db.Timestamptz(3) closedAt DateTime? @db.Timestamptz(3) lockVersion Int @default(0) tenant Tenant @relation(fields: [tenantId], references: [id]) agent Agent @relation(fields: [agentId], references: [id]) channelIdentity ChannelIdentity @relation(fields: [channelIdentityId], references: [id]) assignedToMember TenantMember? @relation("ConversationAssignee", fields: [assignedToMemberId], references: [id]) messages Message[] runs AgentRun[] jobs Job[] lease ConversationProcessingLease? @@index([tenantId, agentId, channelIdentityId]) } model ConversationProcessingLease { conversationId String @id workerId String lockedUntil DateTime @db.Timestamptz(3) heartbeatAt DateTime @db.Timestamptz(3) version BigInt @default(0) // fencing token conversation Conversation @relation(fields: [conversationId], references: [id]) } model Message { id String @id @default(cuid()) conversationId String externalMessageId String? direction MessageDirection senderType SenderType content Json replyToMessageId String? runId String? status MessageStatus @default(received) providerMetadata Json? createdAt DateTime @default(now()) @db.Timestamptz(3) deliveredAt DateTime? @db.Timestamptz(3) readAt DateTime? @db.Timestamptz(3) conversation Conversation @relation(fields: [conversationId], references: [id]) replyTo Message? @relation("ReplyTo", fields: [replyToMessageId], references: [id]) replies Message[] @relation("ReplyTo") run AgentRun? @relation("MessageRun", fields: [runId], references: [id]) triggeredRuns AgentRun[] @relation("RunTriggerMessage") @@index([conversationId]) } model AgentRun { id String @id @default(cuid()) conversationId String agentRevisionId String triggerMessageId String? provider String model String startedAt DateTime @default(now()) @db.Timestamptz(3) finishedAt DateTime? @db.Timestamptz(3) status RunStatus @default(pending) finishReason String? inputTokens Int @default(0) outputTokens Int @default(0) totalTokens Int @default(0) costMicrousd BigInt @default(0) latencyMs Int @default(0) steps Int @default(0) usageRaw Json? toolPolicySnapshot Json? conversation Conversation @relation(fields: [conversationId], references: [id]) revision AgentRevision @relation(fields: [agentRevisionId], references: [id]) triggerMessage Message? @relation("RunTriggerMessage", fields: [triggerMessageId], references: [id]) messages Message[] @relation("MessageRun") toolCalls ToolCall[] @@index([conversationId]) } model ToolCall { id String @id @default(cuid()) runId String toolName String mcpServerId String? arguments Json idempotencyKey String @unique status ToolCallStatus @default(pending) result Json? error String? startedAt DateTime @default(now()) @db.Timestamptz(3) finishedAt DateTime? @db.Timestamptz(3) confirmationRequestedAt DateTime? @db.Timestamptz(3) confirmationExpiresAt DateTime? @db.Timestamptz(3) confirmationMessageId String? confirmedAt DateTime? @db.Timestamptz(3) confirmedByMessageId String? confirmationDecision String? run AgentRun @relation(fields: [runId], references: [id]) mcpServer McpServer? @relation(fields: [mcpServerId], references: [id]) @@index([runId]) } model Job { id String @id @default(cuid()) tenantId String conversationId String type String payload Json status JobStatus @default(pending) availableAt DateTime @default(now()) @db.Timestamptz(3) lockedAt DateTime? @db.Timestamptz(3) lockExpiresAt DateTime? @db.Timestamptz(3) lockedBy String? attempts Int @default(0) maxAttempts Int @default(5) dedupKey String? createdAt DateTime @default(now()) @db.Timestamptz(3) updatedAt DateTime @updatedAt @db.Timestamptz(3) tenant Tenant @relation(fields: [tenantId], references: [id]) conversation Conversation @relation(fields: [conversationId], references: [id]) @@unique([tenantId, dedupKey]) @@index([status, availableAt]) @@index([conversationId]) } model Secret { id String @id @default(cuid()) tenantId String name String encryptedValue String keyVersion Int @default(1) rotationPolicy String? tenant Tenant @relation(fields: [tenantId], references: [id]) channelAccounts ChannelAccount[] @relation("ChannelAccountSecret") @@unique([tenantId, name]) } model AuditLog { id String @id @default(cuid()) tenantId String userId String? action String entity String entityId String? before Json? after Json? createdAt DateTime @default(now()) @db.Timestamptz(3) tenant Tenant @relation(fields: [tenantId], references: [id]) @@index([tenantId, createdAt]) } model Variable { id String @id @default(cuid()) agentId String key String value String agent Agent @relation(fields: [agentId], references: [id]) @@unique([agentId, key]) }