# 06 — Database Schema **Primary database:** PostgreSQL. **ORM:** Prisma. All schema below is conceptual/pseudo-Prisma — refine field types and add indexes during Phase 1 modeling (see [10 — Implementation Roadmap](./10-implementation-roadmap.md)). Cross-cutting requirements for every table below: - Migrations tracked in version control - Indexes on every foreign key and every field used in ticket/queue filtering - Unique constraints where the spec implies natural keys (e.g. `productId` + credential) - Optimistic or transactional concurrency control wherever two actors could race (assignment, SLA state, hierarchy edits) - `createdAt`/`updatedAt` on every table; soft-delete or status field where records must never disappear (audit, escalation events) --- ## Domain: Integration / Catalog ```prisma model Product { id String @id @default(cuid()) externalProductId String @unique // reference into SaaS, not authoritative name String supportEnabled Boolean @default(true) status String // active | suspended | deprecated createdAt DateTime @default(now()) updatedAt DateTime @updatedAt integration ProductIntegration? knowledgeEntries KnowledgeEntry[] runbooks Runbook[] tickets Ticket[] } model ProductIntegration { id String @id @default(cuid()) productId String @unique product Product @relation(fields: [productId], references: [id]) credentialRef String // pointer into secret manager, never the raw secret authMechanism String // signed_token | oauth2_client_credentials | mtls allowedScope Json // structured scope definition rotatedAt DateTime? revokedAt DateTime? createdAt DateTime @default(now()) } model CustomerReference { id String @id @default(cuid()) externalUserId String externalTenantId String createdAt DateTime @default(now()) @@unique([externalUserId, externalTenantId]) } ``` ## Domain: Ticketing ```prisma model Ticket { id String @id @default(cuid()) code String @unique // e.g. DQB-2026-00567 productId String product Product @relation(fields: [productId], references: [id]) problemId String problem Problem @relation(fields: [problemId], references: [id]) externalUserId String externalTenantId String status String // NEW, AI_ANALYZING, AI_TROUBLESHOOTING, AI_VERIFYING, // AI_RESOLVED, HUMAN_ESCALATION, IN_PROGRESS, // WAITING_FOR_CUSTOMER, RESOLUTION_PENDING_CUSTOMER, // RESOLVED, CLOSED, REOPENED priority String severity String categoryId String? problemTypeId String? assignmentId String? slaRunId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt messages TicketMessage[] attachments TicketAttachment[] aiSessions AISupportSession[] assignments Assignment[] escalationEvents EscalationEvent[] } model Problem { id String @id @default(cuid()) statement String symptoms String impact String? productId String featureId String? categoryId String? problemTypeId String? severity String customerImpact String? businessImpact String? environment String? createdAt DateTime @default(now()) tickets Ticket[] investigations Investigation[] rootCauses RootCause[] solutions Solution[] } model TicketMessage { id String @id @default(cuid()) ticketId String ticket Ticket @relation(fields: [ticketId], references: [id]) type String // CUSTOMER_MESSAGE, AI_MESSAGE, AGENT_MESSAGE, INTERNAL_NOTE, // SYSTEM_EVENT, INVESTIGATION_NOTE, SOLUTION_NOTE authorRef String // agentId, "ai", or externalUserId body String visibleToCustomer Boolean @default(true) createdAt DateTime @default(now()) } model TicketAttachment { id String @id @default(cuid()) ticketId String ticket Ticket @relation(fields: [ticketId], references: [id]) storageKey String // S3/MinIO object key, not the file itself fileName String mimeType String sizeBytes Int scanStatus String // pending | clean | infected | rejected uploadedBy String createdAt DateTime @default(now()) } ``` ## Domain: Category / Problem Type / Priority ```prisma model Category { id String @id @default(cuid()) name String productId String? active Boolean @default(true) } model ProblemType { id String @id @default(cuid()) name String categoryId String? active Boolean @default(true) } model PriorityPolicy { id String @id @default(cuid()) name String productId String? categoryId String? rules Json // structured priority derivation rules active Boolean @default(true) } ``` ## Domain: Support Hierarchy / Teams / Agents ```prisma model HierarchyNode { id String @id @default(cuid()) name String parentId String? parent HierarchyNode? @relation("HierarchyTree", fields: [parentId], references: [id]) children HierarchyNode[] @relation("HierarchyTree") order Int teamId String? skills String[] productScope String[] categoryScope String[] priorityScope String[] assignmentStrategy String slaPolicyId String? escalationPolicyId String? entryConditions Json? exitConditions Json? active Boolean @default(true) } model Team { id String @id @default(cuid()) name String active Boolean @default(true) agents Agent[] } model Agent { id String @id @default(cuid()) teamId String team Team @relation(fields: [teamId], references: [id]) name String active Boolean @default(true) skills AgentSkill[] availability AgentAvailability? } model AgentSkill { id String @id @default(cuid()) agentId String agent Agent @relation(fields: [agentId], references: [id]) skillTag String level Int // proficiency, used by SKILL_BASED strategy } model AgentAvailability { id String @id @default(cuid()) agentId String @unique agent Agent @relation(fields: [agentId], references: [id]) status String // available | busy | away | offline workingHours Json // per business calendar currentLoad Int @default(0) } ``` ## Domain: Assignment ```prisma model Assignment { id String @id @default(cuid()) ticketId String ticket Ticket @relation(fields: [ticketId], references: [id]) agentId String strategy String assignedAt DateTime @default(now()) unassignedAt DateTime? reason String? } model AssignmentHistory { id String @id @default(cuid()) ticketId String agentId String? action String // assigned | reassigned | unassigned strategy String reason String? actor String // system | agentId | adminId createdAt DateTime @default(now()) } ``` ## Domain: SLA ```prisma model SLAPolicy { id String @id @default(cuid()) name String productId String? categoryId String? problemTypeId String? priority String? firstResponseMinutes Int investigationMinutes Int? resolutionMinutes Int customerResponseMinutes Int? businessCalendarId String? active Boolean @default(true) } model SLARun { id String @id @default(cuid()) ticketId String @unique policyId String firstResponseDueAt DateTime? resolutionDueAt DateTime? status String // running | paused | warning | breached | completed pausedAt DateTime? resumedAt DateTime? breachedAt DateTime? completedAt DateTime? } model BusinessCalendar { id String @id @default(cuid()) name String timezone String workingHours Json holidays Holiday[] } model Holiday { id String @id @default(cuid()) calendarId String calendar BusinessCalendar @relation(fields: [calendarId], references: [id]) date DateTime description String? } ``` ## Domain: Escalation ```prisma model EscalationPolicy { id String @id @default(cuid()) name String productId String? active Boolean @default(true) rules EscalationRule[] } model EscalationRule { id String @id @default(cuid()) policyId String policy EscalationPolicy @relation(fields: [policyId], references: [id]) triggerType String // first_response_breach | resolution_breach | inactivity | // priority_increase | customer_escalation | repeated_reopen | // manual | product_defect | dependency_timeout | critical_incident condition Json targetNodeId String notify Json // who/how to notify active Boolean @default(true) } model EscalationEvent { id String @id @default(cuid()) ticketId String ticket Ticket @relation(fields: [ticketId], references: [id]) ruleId String? fromNodeId String? toNodeId String? reason String triggeredBy String // system | agentId | customer createdAt DateTime @default(now()) } ``` ## Domain: Problem Resolution ```prisma model Investigation { id String @id @default(cuid()) problemId String problem Problem @relation(fields: [problemId], references: [id]) investigator String findings Json evidence Json? internalNotes String? status String // open | complete createdAt DateTime @default(now()) } model RootCause { id String @id @default(cuid()) problemId String problem Problem @relation(fields: [problemId], references: [id]) type String // technical | configuration | external_dependency | business | contributing_factor description String createdAt DateTime @default(now()) } model Solution { id String @id @default(cuid()) problemId String problem Problem @relation(fields: [problemId], references: [id]) proposed String approved Boolean @default(false) createdAt DateTime @default(now()) implementation SolutionImplementation? verification SolutionVerification? } model SolutionImplementation { id String @id @default(cuid()) solutionId String @unique solution Solution @relation(fields: [solutionId], references: [id]) notes String? implementedBy String implementedAt DateTime @default(now()) } model SolutionVerification { id String @id @default(cuid()) solutionId String @unique solution Solution @relation(fields: [solutionId], references: [id]) method String // automated | technical_test | customer_confirmation | agent_confirmation result String // success | failed evidence Json? verifiedAt DateTime @default(now()) } model Resolution { id String @id @default(cuid()) ticketId String @unique outcome String resolvedBy String // "ai" | agentId resolvedAt DateTime @default(now()) } ``` ## Domain: AI Support ```prisma model AISupportSession { id String @id @default(cuid()) ticketId String ticket Ticket @relation(fields: [ticketId], references: [id]) status String // analyzing | troubleshooting | verifying | resolved | escalated startedAt DateTime @default(now()) endedAt DateTime? diagnoses AIDiagnosis[] interactions AIInteraction[] actions AIAction[] knowledgeRefs AIKnowledgeReference[] } model AIDiagnosis { id String @id @default(cuid()) sessionId String session AISupportSession @relation(fields: [sessionId], references: [id]) product String feature String? problemType String severity String confidence Float possibleCauses String[] createdAt DateTime @default(now()) } model AIInteraction { id String @id @default(cuid()) sessionId String session AISupportSession @relation(fields: [sessionId], references: [id]) role String // customer | ai content String createdAt DateTime @default(now()) } model AIRunbook { id String @id @default(cuid()) key String // e.g. PDF_HTML_CONVERSION_FAILURE productId String steps Json // ordered, versioned step definitions active Boolean @default(true) } model AIAction { id String @id @default(cuid()) sessionId String session AISupportSession @relation(fields: [sessionId], references: [id]) toolName String input Json riskLevel String approvedBy String? // system-policy | agentId, when human approval required createdAt DateTime @default(now()) result AIActionResult? } model AIActionResult { id String @id @default(cuid()) actionId String @unique action AIAction @relation(fields: [actionId], references: [id]) output Json status String // success | failed createdAt DateTime @default(now()) } model AIKnowledgeReference { id String @id @default(cuid()) sessionId String session AISupportSession @relation(fields: [sessionId], references: [id]) knowledgeId String relevanceScore Float? createdAt DateTime @default(now()) } ``` ## Domain: Knowledge ```prisma model KnowledgeEntry { id String @id @default(cuid()) code String @unique // e.g. KB-DQ-102 productId String product Product @relation(fields: [productId], references: [id]) feature String? type String // known_issue | faq | resolution_procedure | operations problem String? symptoms String? errorCode String? cause String? recommendedSolution String? verificationSteps String? escalationGuidance String? version Int @default(1) status String // draft | published | unpublished effectiveDate DateTime? categoryScope String[] validationStatus String // unvalidated | validated owner String? lastReview DateTime? source String? createdAt DateTime @default(now()) } model KnownIssue { id String @id @default(cuid()) productId String errorCodeId String? description String status String } model ErrorCode { id String @id @default(cuid()) code String @unique // e.g. LAYOUT_PARSE_042 productId String description String } model Runbook { id String @id @default(cuid()) key String productId String product Product @relation(fields: [productId], references: [id]) steps Json version Int @default(1) active Boolean @default(true) } ``` ## Domain: Platform ```prisma model Notification { id String @id @default(cuid()) recipientRef String channel String // in_app | email | push event String payload Json status String // queued | sent | failed createdAt DateTime @default(now()) } model AuditLog { id String @id @default(cuid()) actor String actorType String // customer | agent | admin | system | ai action String entityType String entityId String oldValue Json? newValue Json? reason String? metadata Json? createdAt DateTime @default(now()) } ``` --- ## Notes on modeling decisions - **`Ticket` vs `Problem` are always separate tables** with a many-tickets-to-one-problem relationship, per [04](./04-ticketing-and-problem-management.md#2-problem-is-first-class--separate-from-ticket). - **`Investigation`, `RootCause`, `Solution`, `SolutionVerification`, `Resolution` are five distinct models**, not one "resolution notes" text field — this is intentional per the spec and enables reporting on each stage independently. - **`AuditLog` should be append-only** at the application layer: no update/delete code paths against this table, ever. - **SLA timing must never be computed from `SLARun.createdAt` alone** — always resolve through the linked `BusinessCalendar`/`Holiday` records at read time or via a durable recompute job.