generator client { provider = "prisma-client-js" binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") directUrl = env("DIRECT_DATABASE_URL") } // ============================================ // CORE MODELS // ============================================ model User { id String @id @default(uuid()) // Credentials email String @unique password String? type AuthMethod // Relations memberships Membership[] // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("users") } model Project { id String @id @default(uuid()) // Details name String // API public String @unique secret String @unique // Admin disabled Boolean @default(false) // Billing customer String? @unique subscription String? @unique // Billing Limits (per calendar month, null = unlimited) billingLimitWorkflows Int? // Max workflow emails per month billingLimitCampaigns Int? // Max campaign emails per month billingLimitTransactional Int? // Max transactional emails per month // Email Tracking tracking TrackingMode @default(ENABLED) // Open and click tracking mode // Relations members Membership[] contacts Contact[] templates Template[] segments Segment[] workflows Workflow[] campaigns Campaign[] emails Email[] events Event[] domains Domain[] apiRequests ApiRequest[] // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("projects") } model Membership { user User @relation(fields: [userId], references: [id]) userId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) projectId String // Details role Role @default(MEMBER) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([userId, projectId]) @@map("memberships") } model Domain { id String @id @default(uuid()) // Domain details domain String verified Boolean @default(false) // DKIM tokens for DNS verification dkimTokens Json? // Array of DKIM token strings // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) projectId String // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([projectId, domain]) @@index([projectId]) @@index([projectId, verified]) @@map("domains") } // ============================================ // CONTACTS & TEMPLATES // ============================================ model Contact { id String @id @default(uuid()) // Details email String data Json? // Custom fields: { firstName: "John", plan: "pro", ... } // Subscription subscribed Boolean @default(true) // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) projectId String emails Email[] workflowExecutions WorkflowExecution[] events Event[] segmentMemberships SegmentMembership[] // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([projectId, email]) @@index([projectId]) @@index([projectId, subscribed]) @@index([data(ops: JsonbOps)], type: Gin) // GIN index for fast JSON queries on Contact.data @@map("contacts") } model Template { id String @id @default(uuid()) // Details name String description String? // Content subject String body String // HTML content with {{variable}} placeholders from String fromName String? replyTo String? // Type type TemplateType @default(MARKETING) // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) projectId String emails Email[] workflowSteps WorkflowStep[] // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([projectId]) @@index([projectId, type]) @@map("templates") } // ============================================ // SEGMENTS (Dynamic audience groups) // ============================================ model Segment { id String @id @default(uuid()) // Details name String description String? // Filter condition (evaluated dynamically) condition Json // Nested filter structure with AND/OR logic: // { // logic: "OR", // groups: [ // { // filters: [ // { field: "data.plan", operator: "equals", value: "VIP" }, // { field: "subscribed", operator: "equals", value: true } // ] // }, // { // filters: [ // { field: "createdAt", operator: "within", value: 30, unit: "days" } // ], // conditions: { // logic: "AND", // groups: [...] // } // } // ] // } // Operators: equals, notEquals, contains, greaterThan, lessThan, within, exists, etc. // Track membership changes (enables segment entry/exit events) trackMembership Boolean @default(false) // Stats (computed) memberCount Int @default(0) // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) projectId String memberships SegmentMembership[] campaigns Campaign[] // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([projectId]) @@index([condition(ops: JsonbOps)], type: Gin) // GIN index for segment condition queries @@map("segments") } model SegmentMembership { // Relations contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) contactId String segment Segment @relation(fields: [segmentId], references: [id], onDelete: Cascade) segmentId String // Membership tracking enteredAt DateTime @default(now()) exitedAt DateTime? // null = currently in segment // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([contactId, segmentId]) @@index([segmentId, exitedAt]) // Active members (exitedAt = null) @@index([contactId, exitedAt]) @@index([enteredAt]) @@map("segment_memberships") } // ============================================ // CAMPAIGNS (One-time broadcasts) // ============================================ model Campaign { id String @id @default(uuid()) // Details name String description String? status CampaignStatus @default(DRAFT) // Email content subject String body String @db.Text from String fromName String? replyTo String? // Audience selection audienceType CampaignAudienceType @default(ALL) audienceCondition Json? // For FILTERED: manual filter condition with AND/OR logic (same structure as Segment.condition) segment Segment? @relation(fields: [segmentId], references: [id]) segmentId String? // For SEGMENT: reference to saved segment // Scheduling scheduledFor DateTime? // Stats (computed) totalRecipients Int @default(0) sentCount Int @default(0) deliveredCount Int @default(0) openedCount Int @default(0) clickedCount Int @default(0) bouncedCount Int @default(0) // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) projectId String emails Email[] // Timestamps sentAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([projectId, status]) @@index([scheduledFor]) @@index([segmentId]) @@index([audienceCondition(ops: JsonbOps)], type: Gin) // GIN index for campaign audience condition queries @@map("campaigns") } // ============================================ // WORKFLOWS (Automated sequences) // ============================================ model Workflow { id String @id @default(uuid()) // Details name String description String? enabled Boolean @default(false) // Entry point triggerType WorkflowTriggerType triggerConfig Json? // { eventName: "user.signup" } or { schedule: "0 9 * * *" } // Re-entry behavior allowReentry Boolean @default(false) // If true, contacts can enter workflow multiple times // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) projectId String steps WorkflowStep[] executions WorkflowExecution[] // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([projectId]) @@index([projectId, enabled]) @@map("workflows") } model WorkflowStep { id String @id @default(uuid()) // Step details type WorkflowStepType name String position Json // { x: 100, y: 200 } for visual editor // Step configuration (flexible JSON) config Json // Examples by type: // TRIGGER: { eventName: "user.signup" } // SEND_EMAIL: { templateId: "uuid", from: "team@example.com" } // DELAY: { amount: 24, unit: "hours" } // WAIT_FOR_EVENT: { eventName: "email.clicked", timeout: 86400 } // CONDITION: { field: "contact.data.plan", operator: "equals", value: "pro" } // EXIT: { reason: "unsubscribed" } // Relations workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade) workflowId String template Template? @relation(fields: [templateId], references: [id]) templateId String? // For SEND_EMAIL steps // Graph connections outgoingTransitions WorkflowTransition[] @relation("FromStep") incomingTransitions WorkflowTransition[] @relation("ToStep") // Execution tracking executions WorkflowStepExecution[] currentWorkflowExecutions WorkflowExecution[] // Executions currently on this step // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([workflowId]) @@index([templateId]) @@map("workflow_steps") } model WorkflowTransition { id String @id @default(uuid()) // From -> To fromStep WorkflowStep @relation("FromStep", fields: [fromStepId], references: [id], onDelete: Cascade) fromStepId String toStep WorkflowStep @relation("ToStep", fields: [toStepId], references: [id], onDelete: Cascade) toStepId String // Conditional routing condition Json? // null = always follow, or { branch: "yes" } for condition steps priority Int @default(0) // Order to evaluate transitions // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([fromStepId]) @@index([toStepId]) @@map("workflow_transitions") } model WorkflowExecution { id String @id @default(uuid()) // Relations workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade) workflowId String contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) contactId String // Execution state status WorkflowExecutionStatus @default(RUNNING) // Current position currentStep WorkflowStep? @relation(fields: [currentStepId], references: [id]) currentStepId String? // Exit information exitReason String? // Context data (merged with contact.data when executing) context Json? // Additional variables for this execution // Relations stepExecutions WorkflowStepExecution[] emails Email[] // Timestamps startedAt DateTime @default(now()) completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([workflowId, contactId]) // Index for querying executions by workflow and contact @@index([workflowId, status]) @@index([contactId, status]) @@index([status, currentStepId]) @@map("workflow_executions") } model WorkflowStepExecution { id String @id @default(uuid()) // Relations execution WorkflowExecution @relation(fields: [executionId], references: [id], onDelete: Cascade) executionId String step WorkflowStep @relation(fields: [stepId], references: [id], onDelete: Cascade) stepId String // Execution state status StepExecutionStatus @default(PENDING) // Delay/scheduling scheduledFor DateTime? // When this step should execute (for DELAY steps) executeAfter DateTime? // Don't execute before this time (for WAIT_FOR_EVENT timeout) // Result tracking output Json? // Step execution result error String? // Relations emails Email[] // Emails sent by this step execution // Timestamps startedAt DateTime? completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([executionId, status]) @@index([stepId]) @@index([status, scheduledFor]) // For delay queue processor @@index([scheduledFor]) @@map("workflow_step_executions") } // ============================================ // EMAILS (Unified tracking for all sends) // ============================================ model Email { id String @id @default(uuid()) // Recipient contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) contactId String toName String? // Recipient display name for To header // Content (denormalized for history) subject String body String // Rendered HTML from String fromName String? replyTo String? headers Json? // Custom email headers attachments Json? // Array of {filename: string, content: string (base64), contentType: string} // AWS SES Message ID (for tracking webhooks) messageId String? @unique // Source - identifies how this email was triggered sourceType EmailSourceType // Relations to source (one will be set based on sourceType) template Template? @relation(fields: [templateId], references: [id]) templateId String? // For TRANSACTIONAL campaign Campaign? @relation(fields: [campaignId], references: [id], onDelete: Cascade) campaignId String? // For CAMPAIGN workflowExecution WorkflowExecution? @relation(fields: [workflowExecutionId], references: [id], onDelete: Cascade) workflowExecutionId String? // For WORKFLOW workflowStepExecution WorkflowStepExecution? @relation(fields: [workflowStepExecutionId], references: [id], onDelete: Cascade) workflowStepExecutionId String? // For WORKFLOW (specific step) // Delivery status EmailStatus @default(PENDING) // Event timestamps sentAt DateTime? deliveredAt DateTime? openedAt DateTime? // First open clickedAt DateTime? // First click bouncedAt DateTime? complainedAt DateTime? // Event counts opens Int @default(0) clicks Int @default(0) // Error tracking error String? // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) projectId String events Event[] // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([projectId, contactId]) @@index([contactId]) @@index([campaignId]) @@index([workflowExecutionId]) @@index([workflowStepExecutionId]) @@index([status]) @@index([createdAt]) @@index([projectId, sourceType, createdAt]) // For billing limit queries @@index([contactId, openedAt]) // For email activity segment queries @@index([contactId, clickedAt]) // For email activity segment queries @@index([contactId, bouncedAt]) // For email activity segment queries @@index([contactId, complainedAt]) // For email activity segment queries @@map("emails") } // ============================================ // EVENTS (Webhook tracking & workflow triggers) // ============================================ model Event { id String @id @default(uuid()) // Event details name String // e.g., "email.opened", "email.clicked", "user.signup" data Json? // Event payload // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) projectId String contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) contactId String? email Email? @relation(fields: [emailId], references: [id], onDelete: Cascade) emailId String? // Timestamps createdAt DateTime @default(now()) @@index([projectId, name]) @@index([contactId]) @@index([emailId]) @@index([createdAt]) @@index([projectId, contactId, name, createdAt]) // For event-based segment queries (fast!) @@index([contactId, name, createdAt]) // For per-contact event lookups @@index([projectId, name, createdAt]) // For event stats and analytics by type over time @@index([contactId, createdAt]) // For contact activity timeline (recent events) @@index([data(ops: JsonbOps)], type: Gin) // GIN index for event data queries @@map("events") } // ============================================ // ENUMS // ============================================ enum AuthMethod { PASSWORD GOOGLE_OAUTH GITHUB_OAUTH } enum Role { OWNER ADMIN MEMBER } enum TemplateType { TRANSACTIONAL MARKETING } enum TrackingMode { ENABLED // Track opens and clicks for all emails DISABLED // No tracking for any emails MARKETING_ONLY // Track only marketing/campaign emails, not transactional } enum CampaignStatus { DRAFT SCHEDULED SENDING SENT CANCELLED } enum CampaignAudienceType { ALL // All contacts FILTERED // Based on audienceFilter SEGMENT // Predefined segment } enum WorkflowTriggerType { EVENT // Triggered by an event (e.g., user.signup) MANUAL // Manually started via API SCHEDULE // Runs on a schedule (cron) } enum WorkflowStepType { TRIGGER // Entry point SEND_EMAIL // Send an email DELAY // Wait for X time WAIT_FOR_EVENT // Wait for specific event (with timeout) CONDITION // If/else branching EXIT // Early exit point WEBHOOK // Call external webhook UPDATE_CONTACT // Update contact fields } enum WorkflowExecutionStatus { RUNNING // Currently executing WAITING // Waiting for event or delay COMPLETED // Finished normally EXITED // Exited early (via EXIT step) FAILED // Failed with error CANCELLED // Manually cancelled } // ============================================ // API REQUEST LOGGING // ============================================ model ApiRequest { id String @id // Request ID (UUID from X-Request-ID header) // Request metadata method String // HTTP method (GET, POST, etc.) path String // Request path (/v1/send, /contacts, etc.) statusCode Int // HTTP response status code duration Int // Response time in milliseconds // Authentication context projectId String? // Project that made the request (if authenticated) project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull) userId String? // User that made the request (if JWT auth) authType String? // "apiKey" or "jwt" // Request details ip String? // Client IP address userAgent String? // User-Agent header // Error information (if status >= 400) errorCode String? // Machine-readable error code (VALIDATION_ERROR, etc.) errorMessage String? // Human-readable error message // Size tracking (for analytics) requestSize Int? // Request body size in bytes responseSize Int? // Response body size in bytes // Timestamps createdAt DateTime @default(now()) // Indexes for efficient querying @@index([projectId, createdAt(sort: Desc)]) // Query requests by project, most recent first @@index([userId, createdAt(sort: Desc)]) // Query requests by user @@index([statusCode, createdAt(sort: Desc)]) // Query errors (WHERE statusCode >= 400) @@index([path, createdAt(sort: Desc)]) // Query by endpoint @@index([createdAt(sort: Desc)]) // General time-based queries @@index([projectId, statusCode, createdAt(sort: Desc)]) // Composite for project error queries @@map("api_requests") } // ============================================ // ENUMS // ============================================ enum StepExecutionStatus { PENDING // Not yet started SCHEDULED // Scheduled for future execution (DELAY) WAITING // Waiting for event (WAIT_FOR_EVENT) RUNNING // Currently executing COMPLETED // Successfully completed SKIPPED // Skipped (e.g., condition not met) FAILED // Failed with error } enum EmailSourceType { TRANSACTIONAL // Sent via API call CAMPAIGN // Sent as part of broadcast WORKFLOW // Sent via workflow automation } enum EmailStatus { PENDING // Queued for sending SENDING // Currently being sent SENT // Successfully sent to provider DELIVERED // Confirmed delivered OPENED // Recipient opened email CLICKED // Recipient clicked link BOUNCED // Bounced (hard or soft) COMPLAINED // Marked as spam FAILED // Failed to send }