Files
Eric Jungbauer 05aad75272 Phase 1: Forward Assist initial build
Multi-tenant AI help desk SaaS for the firearms industry.
Full monorepo: API (Express/Prisma), Worker (BullMQ), Frontend (React/Vite/Tailwind).
PostgreSQL 16 + pgvector, Redis 7, JWT auth, RLS tenant isolation.
Dark Armory theme with tactical branding throughout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 01:45:22 +00:00

344 lines
13 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================================
// Plans
// ============================================================
model Plan {
id String @id @default(uuid())
name String
slug String @unique
maxUsers Int @default(3)
maxEmailAccounts Int @default(1)
maxTicketsPerMonth Int @default(500)
aiDraftsEnabled Boolean @default(false)
knowledgeBaseEnabled Boolean @default(false)
priceMonthly Decimal @default(0) @db.Decimal(10, 2)
priceYearly Decimal @default(0) @db.Decimal(10, 2)
createdAt DateTime @default(now()) @map("created_at")
tenants Tenant[]
@@map("plans")
}
// ============================================================
// Tenants (Multi-tenant root)
// ============================================================
model Tenant {
id String @id @default(uuid())
name String
slug String @unique
planId String @map("plan_id")
settings Json @default("{}")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
plan Plan @relation(fields: [planId], references: [id])
users User[]
emailAccounts EmailAccount[]
tickets Ticket[]
messages Message[]
aiDrafts AiDraft[]
knowledgeBase KnowledgeBaseEntry[]
auditLogs AuditLog[]
customerProfiles CustomerProfile[]
cannedResponses CannedResponse[]
notificationPreferences NotificationPreference[]
@@map("tenants")
}
// ============================================================
// Users
// ============================================================
model User {
id String @id @default(uuid())
tenantId String @map("tenant_id")
email String
passwordHash String @map("password_hash")
name String
role String @default("agent")
avatarUrl String? @map("avatar_url")
isActive Boolean @default(true) @map("is_active")
lastLoginAt DateTime? @map("last_login_at")
refreshToken String? @map("refresh_token")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
assignedTickets Ticket[] @relation("TicketAssignee")
auditLogs AuditLog[]
notificationPreference NotificationPreference?
createdCannedResponses CannedResponse[]
@@unique([tenantId, email])
@@index([tenantId])
@@map("users")
}
// ============================================================
// Email Accounts
// ============================================================
model EmailAccount {
id String @id @default(uuid())
tenantId String @map("tenant_id")
name String
emailAddress String @map("email_address")
imapHost String @map("imap_host")
imapPort Int @default(993) @map("imap_port")
imapUser String @map("imap_user")
imapPassword String @map("imap_password")
imapTls Boolean @default(true) @map("imap_tls")
smtpHost String @map("smtp_host")
smtpPort Int @default(587) @map("smtp_port")
smtpUser String @map("smtp_user")
smtpPassword String @map("smtp_password")
smtpTls Boolean @default(true) @map("smtp_tls")
isActive Boolean @default(true) @map("is_active")
lastPollAt DateTime? @map("last_poll_at")
lastError String? @map("last_error")
pollIntervalSeconds Int @default(60) @map("poll_interval_seconds")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
tickets Ticket[]
@@unique([tenantId, emailAddress])
@@index([tenantId])
@@map("email_accounts")
}
// ============================================================
// Tickets
// ============================================================
model Ticket {
id String @id @default(uuid())
tenantId String @map("tenant_id")
ticketNumber String @map("ticket_number")
emailAccountId String @map("email_account_id")
subject String
status String @default("incoming")
priority String @default("medium")
assigneeId String? @map("assignee_id")
customerEmail String @map("customer_email")
customerName String? @map("customer_name")
customerProfileId String? @map("customer_profile_id")
tags String[] @default([])
messageCount Int @default(1) @map("message_count")
lastMessageAt DateTime @default(now()) @map("last_message_at")
resolvedAt DateTime? @map("resolved_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
emailAccount EmailAccount @relation(fields: [emailAccountId], references: [id])
assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id])
customerProfile CustomerProfile? @relation(fields: [customerProfileId], references: [id])
messages Message[]
aiDrafts AiDraft[]
@@unique([tenantId, ticketNumber])
@@index([tenantId, status])
@@index([tenantId, assigneeId])
@@index([tenantId, customerEmail])
@@index([tenantId, createdAt])
@@map("tickets")
}
// ============================================================
// Messages
// ============================================================
model Message {
id String @id @default(uuid())
ticketId String @map("ticket_id")
tenantId String @map("tenant_id")
direction String // 'inbound' | 'outbound'
fromEmail String @map("from_email")
fromName String? @map("from_name")
toEmail String @map("to_email")
subject String
bodyText String @map("body_text")
bodyHtml String? @map("body_html")
messageId String? @map("message_id")
inReplyTo String? @map("in_reply_to")
references String?
headers Json?
sentAt DateTime @default(now()) @map("sent_at")
createdAt DateTime @default(now()) @map("created_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
attachments Attachment[]
aiDrafts AiDraft[]
@@index([ticketId])
@@index([tenantId])
@@index([messageId])
@@map("messages")
}
// ============================================================
// Attachments
// ============================================================
model Attachment {
id String @id @default(uuid())
messageId String @map("message_id")
filename String
contentType String @map("content_type")
size Int
storageKey String @map("storage_key")
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
@@map("attachments")
}
// ============================================================
// AI Drafts
// ============================================================
model AiDraft {
id String @id @default(uuid())
ticketId String @map("ticket_id")
tenantId String @map("tenant_id")
messageId String @map("message_id")
draftBody String @map("draft_body")
confidence Float @default(0)
model String @default("gpt-4o")
tokensUsed Int @default(0) @map("tokens_used")
status String @default("pending")
editedBody String? @map("edited_body")
reviewedBy String? @map("reviewed_by")
reviewedAt DateTime? @map("reviewed_at")
createdAt DateTime @default(now()) @map("created_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
@@index([ticketId])
@@index([tenantId])
@@map("ai_drafts")
}
// ============================================================
// Knowledge Base
// ============================================================
model KnowledgeBaseEntry {
id String @id @default(uuid())
tenantId String @map("tenant_id")
title String
content String
category String @default("general")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("knowledge_base")
}
// ============================================================
// Audit Log
// ============================================================
model AuditLog {
id String @id @default(uuid())
tenantId String @map("tenant_id")
userId String? @map("user_id")
action String
entity String
entityId String? @map("entity_id")
details Json?
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
createdAt DateTime @default(now()) @map("created_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id])
@@index([tenantId, createdAt])
@@index([tenantId, entity])
@@map("audit_log")
}
// ============================================================
// Customer Profiles
// ============================================================
model CustomerProfile {
id String @id @default(uuid())
tenantId String @map("tenant_id")
email String
name String?
phone String?
company String?
notes String?
tags String[] @default([])
ticketCount Int @default(0) @map("ticket_count")
firstContactAt DateTime @default(now()) @map("first_contact_at")
lastContactAt DateTime @default(now()) @map("last_contact_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
tickets Ticket[]
@@unique([tenantId, email])
@@index([tenantId])
@@map("customer_profiles")
}
// ============================================================
// Canned Responses
// ============================================================
model CannedResponse {
id String @id @default(uuid())
tenantId String @map("tenant_id")
title String
body String
category String @default("general")
shortcut String?
createdBy String @map("created_by")
isShared Boolean @default(true) @map("is_shared")
usageCount Int @default(0) @map("usage_count")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
creator User @relation(fields: [createdBy], references: [id])
@@index([tenantId])
@@map("canned_responses")
}
// ============================================================
// Notification Preferences
// ============================================================
model NotificationPreference {
id String @id @default(uuid())
userId String @unique @map("user_id")
tenantId String @map("tenant_id")
newTicket Boolean @default(true) @map("new_ticket")
ticketAssigned Boolean @default(true) @map("ticket_assigned")
ticketReply Boolean @default(true) @map("ticket_reply")
aiDraftReady Boolean @default(true) @map("ai_draft_ready")
dailyDigest Boolean @default(false) @map("daily_digest")
emailNotifications Boolean @default(true) @map("email_notifications")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("notification_preferences")
}