Full system blueprint (docs 01-10): product vision, integration & security, AI support architecture, ticketing & problem management, orchestration/SLA/escalation, database schema, backend/frontend architecture, testing/observability/CI-CD, and the implementation roadmap. This is the pre-implementation design reference the codebase is being built against. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
17 KiB
17 KiB
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).
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/updatedAton every table; soft-delete or status field where records must never disappear (audit, escalation events)
Domain: Integration / Catalog
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
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
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
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
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
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
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
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
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
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
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
TicketvsProblemare always separate tables with a many-tickets-to-one-problem relationship, per 04.Investigation,RootCause,Solution,SolutionVerification,Resolutionare five distinct models, not one "resolution notes" text field — this is intentional per the spec and enables reporting on each stage independently.AuditLogshould be append-only at the application layer: no update/delete code paths against this table, ever.- SLA timing must never be computed from
SLARun.createdAtalone — always resolve through the linkedBusinessCalendar/Holidayrecords at read time or via a durable recompute job.