Files
support_backend/docs/06-database-schema.md
saqib mirandClaude Sonnet 5 f475a55a53 docs: add product and engineering specification
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>
2026-08-21 16:22:21 +05:30

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/updatedAt on 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

  • Ticket vs Problem are always separate tables with a many-tickets-to-one-problem relationship, per 04.
  • 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.