// IIOS Interaction Kernel — P1 schema (native portal only). // Lifted from the Bottom-Up Evolution Atlas §10 kernel DDL. ONLY kernel tables: // no support / inbox / route / AI tables (those are earned in later phases). generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ─── Enums (one value per line) ─────────────────────────────────── enum IiosActorKind { HUMAN AI SERVICE BOT UNKNOWN } enum IiosHandleKind { PORTAL_USER EMAIL PHONE WHATSAPP SLACK MATTERMOST TELEGRAM EXTERNAL_VISITOR } enum IiosInteractionKind { MESSAGE EMAIL SYSTEM_NOTICE INBOX_WORK SUPPORT_CASE MEETING_REQUEST DIGEST SUMMARY NOTIFICATION } enum IiosMessagePartKind { TEXT HTML MARKDOWN MEDIA_REF FILE_REF VOICE_REF LOCATION STRUCTURED_JSON } enum IiosDeliveryState { RECEIVED NORMALIZED POLICY_HELD STORED DELIVERED READ FAILED DELETED_LOCAL REDACTED } // ─── Kernel tables ──────────────────────────────────────────────── /// Frozen six-vector scope. orgId + appId are REQUIRED (no global-by-null). model IiosScope { id String @id @default(cuid()) orgId String appId String buId String? tenantId String? workspaceId String? projectId String? userScopeId String? createdAt DateTime @default(now()) sourceHandles IiosSourceHandle[] channels IiosChannel[] threads IiosThread[] interactions IiosInteraction[] @@index([orgId, appId, tenantId]) } /// A channel-specific handle (phone/email/portal user). NOT identity — stays /// UNVERIFIED until MDM resolves it; no silent merge. model IiosSourceHandle { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) kind IiosHandleKind externalId String displayName String? canonicalEntityId String? // set only when MDM resolves; null = unresolved confidence Float @default(0) verificationState String @default("UNVERIFIED") firstSeenAt DateTime @default(now()) lastSeenAt DateTime @default(now()) metadata Json? actors IiosActorRef[] @@unique([scopeId, kind, externalId]) @@index([canonicalEntityId]) } /// The local actor (human / AI / service / unknown) behind an interaction. model IiosActorRef { id String @id @default(cuid()) kind IiosActorKind sourceHandleId String? sourceHandle IiosSourceHandle? @relation(fields: [sourceHandleId], references: [id]) platformPrincipalRef String? canonicalEntityId String? displayName String? createdAt DateTime @default(now()) threadsCreated IiosThread[] @relation("ThreadCreatedBy") participations IiosThreadParticipant[] interactions IiosInteraction[] } /// A configured channel surface (PORTAL in P1). model IiosChannel { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) channelType String channelName String direction String @default("BOTH") capabilityContract Json? externalRef String? enabled Boolean @default(true) createdAt DateTime @default(now()) threads IiosThread[] interactions IiosInteraction[] @@index([scopeId, channelType]) } /// A conversation/thread on a channel. model IiosThread { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) channelId String? channel IiosChannel? @relation(fields: [channelId], references: [id]) threadKind String @default("CONVERSATION") subject String? externalThreadRef String? status String @default("OPEN") visibilityClass String @default("PRIVATE") createdByActorId String? createdByActor IiosActorRef? @relation("ThreadCreatedBy", fields: [createdByActorId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt metadata Json? participants IiosThreadParticipant[] interactions IiosInteraction[] @@index([scopeId, status]) } model IiosThreadParticipant { threadId String thread IiosThread @relation(fields: [threadId], references: [id], onDelete: Cascade) actorId String actor IiosActorRef @relation(fields: [actorId], references: [id]) participantRole String @default("MEMBER") joinedAt DateTime @default(now()) leftAt DateTime? @@id([threadId, actorId]) } /// The semantic envelope. Idempotent per (scope, idempotencyKey). model IiosInteraction { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) kind IiosInteractionKind threadId String? thread IiosThread? @relation(fields: [threadId], references: [id]) channelId String? channel IiosChannel? @relation(fields: [channelId], references: [id]) actorId String? actor IiosActorRef? @relation(fields: [actorId], references: [id]) parentInteractionId String? parent IiosInteraction? @relation("InteractionParent", fields: [parentInteractionId], references: [id]) children IiosInteraction[] @relation("InteractionParent") externalId String? idempotencyKey String occurredAt DateTime @default(now()) receivedAt DateTime @default(now()) status IiosDeliveryState @default(RECEIVED) policyDecisionRef String? consentReceiptRef String? crreBundleRef String? traceId String? metadata Json? parts IiosMessagePart[] @@unique([scopeId, idempotencyKey]) @@index([threadId, occurredAt]) } model IiosMessagePart { id String @id @default(cuid()) interactionId String interaction IiosInteraction @relation(fields: [interactionId], references: [id], onDelete: Cascade) partIndex Int kind IiosMessagePartKind bodyText String? contentRef String? mimeType String? sizeBytes BigInt? checksumSha256 String? metadata Json? @@unique([interactionId, partIndex]) } /// Transactional outbox — written in the same tx as the business row. model IiosOutboxEvent { eventId String @id @default(cuid()) aggregateType String aggregateId String eventType String cloudEvent Json partitionKey String status String @default("PENDING") attempts Int @default(0) nextAttemptAt DateTime @default(now()) createdAt DateTime @default(now()) publishedAt DateTime? @@index([status, nextAttemptAt, createdAt]) } /// Idempotent-consumer ledger for the outbox relay / downstream consumers. model IiosProcessedEvent { consumerName String eventId String processedAt DateTime @default(now()) resultHash String? @@id([consumerName, eventId]) }