Files
plunk/packages/db/prisma/schema.prisma
T

760 lines
20 KiB
Plaintext

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([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
}