Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
1099 lines
40 KiB
Markdown
1099 lines
40 KiB
Markdown
# Outbound Email Abstraction Project Plan
|
|
|
|
## Overview
|
|
|
|
This project introduces a unified abstraction layer for sending outbound emails that supports multiple transport methods, starting with SMTP and Resend API integration. The abstraction will provide a consistent interface while allowing for different email providers based on configuration.
|
|
|
|
## Goals
|
|
|
|
1. **Unified Interface**: Create a single email service interface that can switch between transport methods
|
|
2. **Provider Support**: Initially support SMTP and Resend.com API
|
|
3. **Configuration-Driven**: Allow runtime switching between providers via configuration
|
|
4. **Backwards Compatibility**: Maintain existing email functionality while introducing the new abstraction
|
|
5. **Future Extensibility**: Design for easy addition of other email providers (SendGrid, Mailgun, etc.)
|
|
|
|
## Current System Analysis
|
|
|
|
### Existing Email Infrastructure
|
|
|
|
The Alga PSA application has a comprehensive email system with the following components:
|
|
|
|
#### 1. Email Services (Multiple Implementations)
|
|
- **Primary Service**: `server/src/services/emailService.ts` - Singleton pattern with SMTP via nodemailer
|
|
- **Notification Service**: `server/src/lib/notifications/emailService.ts` - Handlebars template compilation
|
|
- **Event-driven Email**: `server/src/lib/notifications/email.ts` - Comprehensive notification system
|
|
|
|
#### 2. Current Configuration Structure
|
|
```typescript
|
|
// Current environment variables
|
|
EMAIL_ENABLE=false // Global email toggle
|
|
EMAIL_FROM=noreply@example.com
|
|
EMAIL_HOST=smtp.example.com
|
|
EMAIL_PORT=587
|
|
EMAIL_USERNAME=noreply@example.com
|
|
EMAIL_PASSWORD= // Managed via Docker secrets
|
|
```
|
|
|
|
#### 3. Existing Features
|
|
- **Template System**: Handlebars-based with system and tenant-specific templates
|
|
- **Notification Categories**: Hierarchical category/subtype system
|
|
- **User Preferences**: Per-user, per-category notification settings
|
|
- **Rate Limiting**: Per-tenant rate limiting (60 emails/minute default)
|
|
- **Audit Logging**: Comprehensive notification logs with delivery status
|
|
- **Multi-tenant Support**: Row-level security with tenant isolation
|
|
- **Event Bus Integration**: Redis-based asynchronous email processing
|
|
|
|
#### 4. Database Schema
|
|
- `system_email_templates`: System-wide default templates
|
|
- `tenant_email_templates`: Tenant-specific customizations with RLS
|
|
- `notification_settings`: Global tenant settings
|
|
- `notification_categories`: Tenant notification groupings
|
|
- `notification_subtypes`: Specific notification types
|
|
- `user_notification_preferences`: User-level preferences
|
|
- `notification_logs`: Delivery tracking and audit
|
|
|
|
#### 5. Integration Points
|
|
- **Invoice System**: `server/src/lib/jobs/handlers/invoiceEmailHandler.ts`
|
|
- **Ticket Events**: `server/src/lib/eventBus/subscribers/ticketEmailSubscriber.ts`
|
|
- **Authentication**: `server/src/lib/email/sendVerificationEmail.ts`
|
|
- **Event Bus**: Asynchronous processing via Redis streams
|
|
|
|
### Current Limitations
|
|
1. **Single Provider**: Only supports SMTP, no API-based providers
|
|
2. **Separate Services**: Multiple email service implementations with overlapping functionality
|
|
3. **Configuration Complexity**: Multiple environment variables without provider abstraction
|
|
4. **Limited Fallback**: No automatic failover between transport methods
|
|
|
|
## Multi-Tenancy Strategy
|
|
|
|
### Tiered Email Domain Approach
|
|
|
|
Based on enterprise SaaS best practices and PSA industry standards, we will implement a **tiered email domain model**:
|
|
|
|
#### **Tier 1: Platform Domain (Default)**
|
|
- **Target**: Small-medium businesses, new customers
|
|
- **Implementation**: All emails from `tenant-name@yourdomain.com` or `noreply@yourdomain.com`
|
|
- **Benefits**: Zero configuration, immediate functionality, managed reputation
|
|
- **Use Case**: 80% of customers who value simplicity over branding
|
|
|
|
#### **Tier 2: Custom Domain (Enterprise Feature)**
|
|
- **Target**: Enterprise customers, professional service firms
|
|
- **Implementation**: Emails from `support@customer-company.com`
|
|
- **Requirements**: Customer configures DNS (SPF, DKIM, DMARC)
|
|
- **Benefits**: Brand consistency, professional appearance, compliance
|
|
|
|
#### **Tier 3: Hybrid Approach**
|
|
- **Target**: Growing businesses, mixed requirements
|
|
- **Implementation**: Custom domain for client-facing emails, platform domain for internal notifications
|
|
- **Benefits**: Selective branding where it matters most
|
|
|
|
### Centralized Provider Management
|
|
|
|
The underlying infrastructure uses a **centralized provider model**:
|
|
|
|
1. **Single Account Management**: Platform administrator manages all email provider accounts (Resend, SMTP)
|
|
2. **Tenant Isolation**: Each tenant's emails isolated through metadata, headers, and domain verification
|
|
3. **Simplified Setup**: Most tenants need zero email provider configuration
|
|
4. **Enterprise Flexibility**: Custom domain support for customers who require it
|
|
|
|
### Email Flow Examples
|
|
|
|
#### **Tier 1: Platform Domain**
|
|
```
|
|
Tenant "ACME-Corp" → Provider Manager → Resend API
|
|
↓
|
|
Email from: noreply@acme-corp.yourdomain.com
|
|
Reply-to: support@acme-corp.yourdomain.com
|
|
↓
|
|
Client receives professional email with tenant branding
|
|
↓
|
|
Audit logs per tenant in existing system
|
|
```
|
|
|
|
#### **Tier 2: Custom Domain**
|
|
```
|
|
Tenant "ACME-Corp" → Provider Manager → Resend API (Verified Domain)
|
|
↓
|
|
Email from: support@acmecorp.com (Customer's Domain)
|
|
DKIM signed by: acmecorp.com
|
|
↓
|
|
Client receives email from customer's actual domain
|
|
↓
|
|
Audit logs per tenant + domain verification status
|
|
```
|
|
|
|
### Key Multi-Tenancy Considerations
|
|
|
|
1. **Tenant Isolation**:
|
|
- All email logs separated by `tenant` column (existing RLS)
|
|
- Email templates isolated per tenant (existing system)
|
|
- Rate limiting per tenant (existing system)
|
|
- Provider-level metadata tagging for tracking
|
|
|
|
2. **From Address Management**:
|
|
- **Tier 1**: Platform-managed subdomains (`tenant-name@yourdomain.com`)
|
|
- **Tier 2**: Customer-owned domains (`support@customer.com`)
|
|
- **Tier 3**: Mixed approach based on email type
|
|
|
|
3. **Domain Verification & Security**:
|
|
- Platform domains: Pre-verified in Resend
|
|
- Custom domains: Customer completes DNS verification process
|
|
- SPF/DKIM/DMARC configuration handled per domain
|
|
- Reputation isolation between domains
|
|
|
|
4. **Compliance & Tracking**:
|
|
- Provider-level webhook handling for delivery status
|
|
- Tenant-specific bounce/complaint handling
|
|
- Domain-specific reputation monitoring
|
|
- Audit trails maintained per tenant in existing `notification_logs`
|
|
|
|
## Technical Design
|
|
|
|
### Integration Strategy
|
|
|
|
The abstraction will be implemented as a **centralized provider layer** that:
|
|
|
|
1. **Minimal Disruption**: Existing email service interfaces remain unchanged
|
|
2. **Gradual Migration**: Services can be migrated one at a time
|
|
3. **Centralized Management**: Single provider accounts managed by platform admins
|
|
4. **Tenant Transparency**: Tenants unaware of underlying provider complexity
|
|
|
|
### Core Components
|
|
|
|
#### 1. Email Provider Interface
|
|
```typescript
|
|
interface IEmailProvider {
|
|
sendEmail(emailData: EmailData): Promise<EmailResult>;
|
|
validateConfig(): Promise<boolean>;
|
|
getProviderName(): string;
|
|
isHealthy(): Promise<boolean>;
|
|
}
|
|
```
|
|
|
|
#### 2. Email Data Models (Enhanced for Multi-Tenancy)
|
|
```typescript
|
|
interface EmailData {
|
|
to: string | string[];
|
|
from: string;
|
|
subject: string;
|
|
html?: string;
|
|
text?: string;
|
|
attachments?: EmailAttachment[];
|
|
headers?: Record<string, string>;
|
|
replyTo?: string;
|
|
cc?: string | string[];
|
|
bcc?: string | string[];
|
|
// Multi-tenant metadata (required)
|
|
tenantId: string; // Always required for tenant isolation
|
|
userId?: string; // Optional user context
|
|
templateName?: string; // For template tracking
|
|
// Provider-level tracking
|
|
tags?: string[]; // Provider-specific tags for categorization
|
|
}
|
|
|
|
interface EmailResult {
|
|
success: boolean;
|
|
messageId?: string;
|
|
error?: string;
|
|
provider: string;
|
|
timestamp: Date;
|
|
tenantId: string; // Tenant context for audit logs
|
|
metadata?: {
|
|
resendId?: string; // Resend-specific message ID
|
|
smtpResponse?: string; // SMTP server response
|
|
tags?: string[]; // Applied tags
|
|
webhookId?: string; // For webhook correlation
|
|
};
|
|
}
|
|
|
|
interface EmailAttachment {
|
|
filename: string;
|
|
path?: string; // For file path attachments
|
|
content?: Buffer; // For inline content
|
|
contentType: string;
|
|
cid?: string; // For embedded images
|
|
}
|
|
|
|
// Tenant-specific configuration (managed by platform)
|
|
interface TenantEmailSettings {
|
|
tenantId: string;
|
|
emailTier: 'platform' | 'custom' | 'hybrid'; // Email domain tier
|
|
|
|
// Platform Domain Settings (Tier 1)
|
|
platformDomain?: string; // acme-corp.yourdomain.com
|
|
defaultFromAddress: string; // noreply@acme-corp.yourdomain.com
|
|
|
|
// Custom Domain Settings (Tier 2)
|
|
customDomain?: string; // acmecorp.com
|
|
customFromAddress?: string; // support@acmecorp.com
|
|
domainVerified: boolean; // DNS verification status
|
|
dkimEnabled: boolean; // DKIM signing status
|
|
|
|
// Common Settings
|
|
replyToAddress?: string; // tenant-specific reply-to
|
|
customHeaders?: Record<string, string>; // tenant-specific headers
|
|
tags?: string[]; // tenant-specific tags for all emails
|
|
|
|
// Email Type Routing (Tier 3 - Hybrid)
|
|
emailTypeRouting?: {
|
|
invoices: 'platform' | 'custom'; // Where to send invoices
|
|
notifications: 'platform' | 'custom'; // Where to send notifications
|
|
tickets: 'platform' | 'custom'; // Where to send ticket updates
|
|
};
|
|
}
|
|
```
|
|
|
|
#### 3. Provider Implementations
|
|
|
|
##### SMTP Provider (Existing Compatibility)
|
|
```typescript
|
|
export class SMTPEmailProvider implements IEmailProvider {
|
|
constructor(private config: SMTPConfig) {}
|
|
|
|
async sendEmail(emailData: EmailData): Promise<EmailResult> {
|
|
// Leverage existing nodemailer logic from current services
|
|
// Maintain compatibility with current SMTP configuration
|
|
}
|
|
}
|
|
```
|
|
|
|
##### Resend Provider (Centralized Multi-Tenant)
|
|
```typescript
|
|
export class ResendEmailProvider implements IEmailProvider {
|
|
private resend: Resend;
|
|
private tenantSettingsCache: Map<string, TenantEmailSettings>;
|
|
|
|
constructor(private config: ResendConfig) {
|
|
this.resend = new Resend(config.apiKey);
|
|
this.tenantSettingsCache = new Map();
|
|
}
|
|
|
|
async sendEmail(emailData: EmailData): Promise<EmailResult> {
|
|
// Get tenant-specific settings
|
|
const tenantSettings = await this.getTenantSettings(emailData.tenantId);
|
|
|
|
// Apply tenant-specific email configuration
|
|
const resendData = {
|
|
from: this.buildFromAddress(emailData.from, tenantSettings),
|
|
to: emailData.to,
|
|
subject: emailData.subject,
|
|
html: emailData.html,
|
|
text: emailData.text,
|
|
headers: {
|
|
...emailData.headers,
|
|
...tenantSettings.customHeaders,
|
|
'X-Tenant-ID': emailData.tenantId, // For webhook correlation
|
|
'X-User-ID': emailData.userId || '',
|
|
},
|
|
tags: [
|
|
`tenant:${emailData.tenantId}`,
|
|
...(tenantSettings.tags || []),
|
|
...(emailData.tags || []),
|
|
],
|
|
};
|
|
|
|
const result = await this.resend.emails.send(resendData);
|
|
|
|
return {
|
|
success: true,
|
|
messageId: result.id,
|
|
provider: 'resend',
|
|
timestamp: new Date(),
|
|
tenantId: emailData.tenantId,
|
|
metadata: {
|
|
resendId: result.id,
|
|
tags: resendData.tags,
|
|
}
|
|
};
|
|
}
|
|
|
|
private async getTenantSettings(tenantId: string): Promise<TenantEmailSettings> {
|
|
// Cache tenant settings for performance
|
|
if (this.tenantSettingsCache.has(tenantId)) {
|
|
return this.tenantSettingsCache.get(tenantId)!;
|
|
}
|
|
|
|
// Load from database or configuration
|
|
const settings = await this.loadTenantEmailSettings(tenantId);
|
|
this.tenantSettingsCache.set(tenantId, settings);
|
|
return settings;
|
|
}
|
|
|
|
private buildFromAddress(requestedFrom: string, settings: TenantEmailSettings): string {
|
|
// Ensure all emails come from verified domains
|
|
if (requestedFrom.includes(settings.fromDomain)) {
|
|
return requestedFrom;
|
|
}
|
|
return settings.defaultFromAddress;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 4. Centralized Provider Manager
|
|
```typescript
|
|
export class EmailProviderManager {
|
|
private primaryProvider: IEmailProvider;
|
|
private fallbackProvider?: IEmailProvider;
|
|
private tenantService: TenantEmailSettingsService;
|
|
|
|
constructor(config: EmailProviderConfig) {
|
|
this.primaryProvider = this.createProvider(config.primary);
|
|
if (config.fallback) {
|
|
this.fallbackProvider = this.createProvider(config.fallback);
|
|
}
|
|
this.tenantService = new TenantEmailSettingsService();
|
|
}
|
|
|
|
async sendEmail(emailData: EmailData): Promise<EmailResult> {
|
|
// Validate tenant context
|
|
if (!emailData.tenantId) {
|
|
throw new Error('Tenant ID is required for all email operations');
|
|
}
|
|
|
|
// Apply tenant-specific rate limiting (using existing system)
|
|
await this.checkTenantRateLimit(emailData.tenantId, emailData.userId);
|
|
|
|
try {
|
|
const result = await this.primaryProvider.sendEmail(emailData);
|
|
|
|
// Log to tenant-specific audit trail
|
|
await this.logEmailResult(result);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
if (this.fallbackProvider) {
|
|
const result = await this.fallbackProvider.sendEmail(emailData);
|
|
await this.logEmailResult(result);
|
|
return result;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async checkTenantRateLimit(tenantId: string, userId?: string): Promise<void> {
|
|
// Leverage existing notification system rate limiting
|
|
// This integrates with the current tenant-aware rate limiting
|
|
}
|
|
|
|
private async logEmailResult(result: EmailResult): Promise<void> {
|
|
// Log to existing notification_logs table with tenant isolation
|
|
// Maintains existing audit trail functionality
|
|
}
|
|
}
|
|
```
|
|
|
|
## Resend Custom Domain Management
|
|
|
|
Based on the Context7 documentation, Resend handles custom domains through:
|
|
|
|
### **Domain Verification Architecture**
|
|
Resend uses a **subdomain-based approach** to avoid conflicts with existing email infrastructure:
|
|
|
|
```bash
|
|
# Customer adds these DNS records for domain verification:
|
|
|
|
# 1. DKIM Authentication (Required)
|
|
resend._domainkey.customer.com TXT "resend-generated-dkim-key"
|
|
|
|
# 2. SPF Authorization (Required)
|
|
send.customer.com TXT "v=spf1 include:resend.net ~all"
|
|
|
|
# 3. MX Record for Bounce Handling (Required)
|
|
send.customer.com MX 10 feedback-smtp.us-east-1.amazonses.com
|
|
|
|
# 4. DMARC Policy (Recommended)
|
|
_dmarc.customer.com TXT "v=DMARC1; p=none; rua=mailto:dmarcreports@customer.com;"
|
|
```
|
|
|
|
### **Multi-Tenant Domain Support**
|
|
- **Single Account**: One Resend account can manage **multiple domains** for different tenants
|
|
- **Domain Isolation**: Each verified domain has separate reputation and deliverability metrics
|
|
- **Regional Support**: Supports multiple AWS regions (us-east-1, eu-west-1, ap-northeast-1, sa-east-1)
|
|
- **Webhook Isolation**: Domain-specific webhook endpoints for bounce/complaint handling
|
|
|
|
### **Implementation for Multi-Tenancy**
|
|
```typescript
|
|
export class ResendEmailProvider implements IEmailProvider {
|
|
private resend: Resend;
|
|
private verifiedDomains: Set<string>;
|
|
|
|
constructor(config: ResendConfig) {
|
|
this.resend = new Resend(config.apiKey);
|
|
this.verifiedDomains = new Set();
|
|
}
|
|
|
|
async sendEmail(emailData: EmailData): Promise<EmailResult> {
|
|
const tenantSettings = await this.getTenantSettings(emailData.tenantId);
|
|
|
|
// Determine which domain to use
|
|
const fromDomain = this.selectDomain(emailData, tenantSettings);
|
|
|
|
// Validate domain is verified
|
|
if (tenantSettings.emailTier === 'custom' && !tenantSettings.domainVerified) {
|
|
throw new Error(`Custom domain ${tenantSettings.customDomain} not verified`);
|
|
}
|
|
|
|
const result = await this.resend.emails.send({
|
|
from: this.buildFromAddress(emailData.from, fromDomain),
|
|
to: emailData.to,
|
|
subject: emailData.subject,
|
|
html: emailData.html,
|
|
text: emailData.text,
|
|
headers: {
|
|
...emailData.headers,
|
|
'X-Tenant-ID': emailData.tenantId,
|
|
},
|
|
tags: [
|
|
`tenant:${emailData.tenantId}`,
|
|
`domain:${fromDomain}`,
|
|
...(emailData.tags || [])
|
|
]
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
messageId: result.id,
|
|
provider: 'resend',
|
|
timestamp: new Date(),
|
|
tenantId: emailData.tenantId,
|
|
metadata: {
|
|
resendId: result.id,
|
|
domain: fromDomain,
|
|
tags: result.tags
|
|
}
|
|
};
|
|
}
|
|
|
|
private selectDomain(emailData: EmailData, settings: TenantEmailSettings): string {
|
|
switch (settings.emailTier) {
|
|
case 'custom':
|
|
return settings.customDomain!;
|
|
case 'platform':
|
|
default:
|
|
return settings.platformDomain || 'yourdomain.com';
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### **Fully Automated Domain Verification Workflow**
|
|
|
|
**Complete API Coverage**: Resend provides **full API automation** for domain creation, verification, and management.
|
|
|
|
#### **Automated Domain Management Service:**
|
|
```typescript
|
|
export class ResendDomainService {
|
|
private resend: Resend;
|
|
|
|
constructor(apiKey: string) {
|
|
this.resend = new Resend(apiKey);
|
|
}
|
|
|
|
async createTenantDomain(tenantId: string, domain: string): Promise<DomainSetup> {
|
|
try {
|
|
// 1. Create domain in Resend via API
|
|
const domainResult = await this.resend.domains.create({
|
|
name: domain,
|
|
region: 'us-east-1' // or tenant-specific region
|
|
});
|
|
|
|
// 2. Store domain info and DNS records in database
|
|
await this.storeDomainSetup(tenantId, {
|
|
resendDomainId: domainResult.id,
|
|
domain: domainResult.name,
|
|
status: domainResult.status,
|
|
dnsRecords: domainResult.records,
|
|
region: domainResult.region
|
|
});
|
|
|
|
// 3. Send DNS configuration instructions to tenant
|
|
await this.sendDNSInstructions(tenantId, domainResult.records);
|
|
|
|
return domainResult;
|
|
} catch (error) {
|
|
throw new Error(`Failed to create domain: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async checkDomainVerification(tenantId: string): Promise<DomainStatus> {
|
|
const domainSetup = await this.getTenantDomainSetup(tenantId);
|
|
|
|
// Get current status from Resend API
|
|
const domainInfo = await this.resend.domains.get(domainSetup.resendDomainId);
|
|
|
|
// Update local database with current status
|
|
await this.updateDomainStatus(tenantId, domainInfo.status);
|
|
|
|
return {
|
|
domain: domainInfo.name,
|
|
status: domainInfo.status, // 'not_started' | 'pending' | 'verified' | 'failed'
|
|
records: domainInfo.records,
|
|
verifiedAt: domainInfo.status === 'verified' ? new Date() : null
|
|
};
|
|
}
|
|
|
|
async triggerDomainVerification(tenantId: string): Promise<boolean> {
|
|
const domainSetup = await this.getTenantDomainSetup(tenantId);
|
|
|
|
try {
|
|
// Trigger verification check via API
|
|
await this.resend.domains.verify(domainSetup.resendDomainId);
|
|
|
|
// Check updated status
|
|
const status = await this.checkDomainVerification(tenantId);
|
|
return status.status === 'verified';
|
|
} catch (error) {
|
|
console.error('Domain verification failed:', error);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### **Workflow-Managed Domain Verification Process:**
|
|
|
|
Using the existing workflow system to orchestrate the entire domain verification lifecycle:
|
|
|
|
```typescript
|
|
// System workflow for domain verification management
|
|
async function domainVerificationWorkflow(context: WorkflowContext): Promise<void> {
|
|
const { actions, events, logger, setState, data } = context;
|
|
const { tenantId, domain } = context.input.triggerEvent.payload;
|
|
|
|
setState('DOMAIN_CREATION_STARTED');
|
|
logger.info(`Starting domain verification workflow for ${domain}`);
|
|
|
|
try {
|
|
// 1. Create domain in Resend via API
|
|
const domainResult = await actions.createResendDomain({
|
|
tenantId,
|
|
domain,
|
|
region: 'us-east-1'
|
|
});
|
|
|
|
data.set('domainSetup', domainResult);
|
|
setState('DOMAIN_CREATED_DNS_PENDING');
|
|
|
|
// 2. Send DNS instructions to tenant
|
|
await actions.sendDNSInstructions({
|
|
tenantId,
|
|
domain,
|
|
dnsRecords: domainResult.dnsRecords
|
|
});
|
|
|
|
// 3. Wait for tenant confirmation or timeout
|
|
setState('AWAITING_DNS_CONFIGURATION');
|
|
const dnsConfiguredEvent = await events.waitFor(
|
|
'DNS_CONFIGURED',
|
|
60 * 60 * 1000 // 1 hour timeout
|
|
);
|
|
|
|
if (!dnsConfiguredEvent) {
|
|
// Create inline human task for follow-up if tenant doesn't respond
|
|
const taskResult = await actions.createInlineTaskAndWaitForResult({
|
|
title: `Follow up on DNS configuration for ${domain}`,
|
|
description: `DNS configuration timeout. Please complete DNS setup for domain ${domain} or contact support if you need assistance.`,
|
|
formDefinition: {
|
|
jsonSchema: {
|
|
type: 'object',
|
|
title: 'DNS Configuration Follow-up',
|
|
properties: {
|
|
domainInfo: {
|
|
type: 'string',
|
|
title: 'Domain Information',
|
|
readOnly: true,
|
|
default: `Domain: ${domain}\nWaiting Time: 1 hour\nStatus: DNS configuration timeout`
|
|
},
|
|
dnsRecordsDisplay: {
|
|
type: 'string',
|
|
title: 'Required DNS Records',
|
|
readOnly: true,
|
|
default: `${JSON.stringify(domainResult.dnsRecords, null, 2)}`
|
|
},
|
|
userAction: {
|
|
type: 'string',
|
|
title: 'What would you like to do?',
|
|
enum: ['continue_waiting', 'cancel_setup', 'contact_support'],
|
|
enumNames: ['Continue waiting (DNS records are configured)', 'Cancel domain setup', 'Contact support for assistance']
|
|
},
|
|
notes: {
|
|
type: 'string',
|
|
title: 'Additional Notes (optional)',
|
|
description: 'Any additional information about the DNS configuration'
|
|
}
|
|
},
|
|
required: ['userAction']
|
|
}
|
|
},
|
|
contextData: {
|
|
tenantId,
|
|
domain,
|
|
dnsRecords: domainResult.dnsRecords,
|
|
hoursWaiting: 1
|
|
},
|
|
waitForEventTimeoutMilliseconds: 24 * 60 * 60 * 1000 // 24 hours
|
|
});
|
|
|
|
if (!taskResult.success || taskResult.resolutionData?.userAction === 'cancel_setup') {
|
|
setState('DNS_CONFIGURATION_ABANDONED');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 4. Begin verification polling loop
|
|
setState('VERIFYING_DOMAIN');
|
|
let verificationAttempts = 0;
|
|
const maxAttempts = 20; // 10 minutes total (30s intervals)
|
|
|
|
while (verificationAttempts < maxAttempts) {
|
|
verificationAttempts++;
|
|
|
|
// Trigger verification check in Resend
|
|
const verificationResult = await actions.triggerDomainVerification({
|
|
tenantId,
|
|
resendDomainId: domainResult.resendDomainId
|
|
});
|
|
|
|
if (verificationResult.status === 'verified') {
|
|
setState('DOMAIN_VERIFIED');
|
|
|
|
// 5. Activate domain for email sending
|
|
await actions.activateCustomDomain({
|
|
tenantId,
|
|
domain,
|
|
resendDomainId: domainResult.resendDomainId
|
|
});
|
|
|
|
// 6. Notify tenant of successful verification
|
|
await actions.sendDomainVerificationSuccess({
|
|
tenantId,
|
|
domain
|
|
});
|
|
|
|
setState('DOMAIN_ACTIVE');
|
|
logger.info(`Domain ${domain} successfully verified and activated`);
|
|
return;
|
|
}
|
|
|
|
if (verificationResult.status === 'failed') {
|
|
// Create human task for DNS troubleshooting
|
|
const troubleshootResult = await actions.createInlineTaskAndWaitForResult({
|
|
title: `DNS verification failed for ${domain}`,
|
|
description: `Domain verification has failed. Please review your DNS settings and try again.`,
|
|
formDefinition: {
|
|
jsonSchema: {
|
|
type: 'object',
|
|
title: 'DNS Verification Failed',
|
|
properties: {
|
|
failureInfo: {
|
|
type: 'string',
|
|
title: 'Failure Information',
|
|
readOnly: true,
|
|
default: `Domain: ${domain}\nFailure Reason: ${verificationResult.failureReason}\nAttempt: ${verificationAttempts} of ${maxAttempts}`
|
|
},
|
|
dnsRecordsDisplay: {
|
|
type: 'string',
|
|
title: 'Required DNS Records',
|
|
readOnly: true,
|
|
default: `${JSON.stringify(domainResult.dnsRecords, null, 2)}`
|
|
},
|
|
nextAction: {
|
|
type: 'string',
|
|
title: 'What would you like to do?',
|
|
enum: ['retry_verification', 'cancel_setup', 'contact_support'],
|
|
enumNames: ['Retry verification (I\'ve fixed the DNS)', 'Cancel domain setup', 'Contact support for help']
|
|
},
|
|
troubleshootingNotes: {
|
|
type: 'string',
|
|
title: 'Troubleshooting Notes (optional)',
|
|
description: 'Any changes you made or issues you encountered'
|
|
}
|
|
},
|
|
required: ['nextAction']
|
|
}
|
|
},
|
|
contextData: {
|
|
tenantId,
|
|
domain,
|
|
failureReason: verificationResult.failureReason,
|
|
dnsRecords: domainResult.dnsRecords,
|
|
currentAttempt: verificationAttempts
|
|
},
|
|
waitForEventTimeoutMilliseconds: 2 * 60 * 60 * 1000 // 2 hours
|
|
});
|
|
|
|
if (troubleshootResult.success && troubleshootResult.resolutionData?.nextAction === 'retry_verification') {
|
|
// Reset attempt counter and continue
|
|
verificationAttempts = 0;
|
|
setState('RETRYING_VERIFICATION');
|
|
continue;
|
|
} else {
|
|
setState('DOMAIN_VERIFICATION_FAILED');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Wait 30 seconds before next attempt
|
|
await new Promise(resolve => setTimeout(resolve, 30000));
|
|
}
|
|
|
|
// Max attempts reached - escalate to human task
|
|
await actions.createInlineTaskAndWaitForResult({
|
|
title: `Domain verification timeout for ${domain}`,
|
|
description: `Domain verification has timed out after ${maxAttempts} attempts. Manual intervention may be required.`,
|
|
formDefinition: {
|
|
jsonSchema: {
|
|
type: 'object',
|
|
title: 'Domain Verification Timeout',
|
|
properties: {
|
|
timeoutInfo: {
|
|
type: 'string',
|
|
title: 'Timeout Information',
|
|
readOnly: true,
|
|
default: `Domain: ${domain}\nAttempts Completed: ${maxAttempts}\nTotal Time: ~${Math.round(maxAttempts * 30 / 60)} minutes`
|
|
},
|
|
dnsRecordsDisplay: {
|
|
type: 'string',
|
|
title: 'Required DNS Records',
|
|
readOnly: true,
|
|
default: `${JSON.stringify(domainResult.dnsRecords, null, 2)}`
|
|
},
|
|
resolution: {
|
|
type: 'string',
|
|
title: 'How would you like to proceed?',
|
|
enum: ['manual_verification', 'cancel_setup', 'escalate_support'],
|
|
enumNames: ['Manually verify domain (override)', 'Cancel domain setup', 'Escalate to technical support']
|
|
},
|
|
notes: {
|
|
type: 'string',
|
|
title: 'Additional Information',
|
|
description: 'Any additional context about the timeout or domain setup'
|
|
}
|
|
},
|
|
required: ['resolution']
|
|
}
|
|
},
|
|
contextData: {
|
|
tenantId,
|
|
domain,
|
|
attemptsCompleted: maxAttempts,
|
|
dnsRecords: domainResult.dnsRecords
|
|
}
|
|
});
|
|
|
|
setState('DOMAIN_VERIFICATION_TIMEOUT');
|
|
|
|
} catch (error) {
|
|
logger.error(`Domain verification workflow failed: ${error.message}`);
|
|
setState('DOMAIN_VERIFICATION_ERROR');
|
|
|
|
// Create error resolution task
|
|
await actions.createInlineTaskAndWaitForResult({
|
|
title: `Domain verification error for ${domain}`,
|
|
description: `An unexpected error occurred during domain verification. Technical support may be required.`,
|
|
formDefinition: {
|
|
jsonSchema: {
|
|
type: 'object',
|
|
title: 'Domain Verification Error',
|
|
properties: {
|
|
errorInfo: {
|
|
type: 'string',
|
|
title: 'Error Information',
|
|
readOnly: true,
|
|
default: `Domain: ${domain}\nError: ${error.message}\nWorkflow Execution: ${context.executionId}`
|
|
},
|
|
errorDetails: {
|
|
type: 'string',
|
|
title: 'Technical Details',
|
|
readOnly: true,
|
|
default: JSON.stringify({
|
|
tenantId,
|
|
domain,
|
|
error: error.message,
|
|
stack: error.stack,
|
|
timestamp: new Date().toISOString()
|
|
}, null, 2)
|
|
},
|
|
nextStep: {
|
|
type: 'string',
|
|
title: 'Next Steps',
|
|
enum: ['retry_workflow', 'cancel_setup', 'escalate_technical'],
|
|
enumNames: ['Retry domain verification workflow', 'Cancel domain setup', 'Escalate to technical team']
|
|
},
|
|
adminNotes: {
|
|
type: 'string',
|
|
title: 'Admin Notes',
|
|
description: 'Any additional context or troubleshooting steps taken'
|
|
}
|
|
},
|
|
required: ['nextStep']
|
|
}
|
|
},
|
|
contextData: {
|
|
tenantId,
|
|
domain,
|
|
error: error.message,
|
|
workflowExecutionId: context.executionId
|
|
}
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
#### **Workflow-Driven Benefits:**
|
|
1. **Automated Orchestration**: Complete automation of the domain verification process
|
|
2. **Human Task Integration**: Automatic escalation when manual intervention needed
|
|
3. **Timeout Management**: Built-in timeout handling with human task creation
|
|
4. **Retry Logic**: Intelligent retry mechanisms with human oversight
|
|
5. **Audit Trail**: Complete event-sourced history of domain verification process
|
|
6. **Error Recovery**: Graceful error handling with human task escalation
|
|
7. **Status Tracking**: Real-time workflow state updates visible in admin dashboard
|
|
|
|
### **Benefits for Multi-Tenant SaaS**
|
|
- **Reputation Isolation**: Each tenant's domain has independent reputation
|
|
- **Brand Consistency**: Tenants can send from their own domains
|
|
- **Compliance**: Meets enterprise requirements for branded communications
|
|
- **Scalability**: Single Resend account can manage hundreds of tenant domains
|
|
- **Cost Efficiency**: No need for separate Resend accounts per tenant
|
|
|
|
## Implementation Plan
|
|
|
|
### Phase 1: Foundation & Provider Abstraction (Week 1-2)
|
|
1. **Provider Interface**: Create `IEmailProvider` interface and base types
|
|
2. **SMTP Provider**: Extract existing SMTP logic into `SMTPEmailProvider`
|
|
3. **Provider Manager**: Implement `EmailProviderManager` with fallback support
|
|
4. **Configuration Schema**: Design unified configuration structure
|
|
5. **Testing Framework**: Set up unit tests for provider abstraction
|
|
|
|
**Deliverables:**
|
|
- `server/src/lib/email/providers/IEmailProvider.ts`
|
|
- `server/src/lib/email/providers/SMTPEmailProvider.ts`
|
|
- `server/src/lib/email/EmailProviderManager.ts`
|
|
- Unit tests for SMTP provider maintaining existing functionality
|
|
|
|
### Phase 2: Resend Integration (Week 2-3)
|
|
1. **Resend Provider**: Implement `ResendEmailProvider` with full API integration
|
|
2. **Environment Variables**: Add Resend configuration to environment schema
|
|
3. **Configuration Migration**: Update existing services to use provider manager
|
|
4. **Rate Limiting**: Implement Resend-specific rate limiting and quota management
|
|
5. **Testing**: Create integration tests with Resend test endpoints
|
|
|
|
**Deliverables:**
|
|
- `server/src/lib/email/providers/ResendEmailProvider.ts`
|
|
- Updated environment configuration
|
|
- Integration tests with Resend sandbox
|
|
- Documentation for Resend setup and domain verification
|
|
|
|
### Phase 3: Service Integration & Migration (Week 3-4)
|
|
1. **Service Refactoring**: Update existing email services to use provider manager
|
|
- Migrate `server/src/services/emailService.ts`
|
|
- Update `server/src/lib/notifications/emailService.ts`
|
|
- Integrate with notification system in `server/src/lib/notifications/email.ts`
|
|
2. **Configuration Management**: Implement runtime provider switching
|
|
3. **Error Handling**: Add comprehensive error handling and retry logic
|
|
4. **Monitoring Integration**: Add provider-specific logging and metrics
|
|
|
|
**Deliverables:**
|
|
- Refactored email services using provider abstraction
|
|
- Configuration management for provider selection
|
|
- Enhanced error handling and retry mechanisms
|
|
- Provider health monitoring
|
|
|
|
### Phase 4: Testing, Documentation & Deployment (Week 4-5)
|
|
1. **End-to-End Testing**: Comprehensive testing of all email flows
|
|
2. **Performance Testing**: Compare provider performance and reliability
|
|
3. **Documentation**: Complete API documentation and deployment guides
|
|
4. **Migration Guide**: Create step-by-step migration instructions
|
|
5. **Monitoring Dashboard**: Implement email provider monitoring
|
|
|
|
**Deliverables:**
|
|
- Complete test suite covering all email scenarios
|
|
- Performance benchmarks and recommendations
|
|
- Updated deployment documentation
|
|
- Migration guide for existing installations
|
|
- Provider monitoring and alerting
|
|
|
|
## Configuration Structure
|
|
|
|
### Unified Configuration Schema
|
|
```typescript
|
|
interface EmailProviderConfig {
|
|
enabled: boolean;
|
|
primary: ProviderConfig;
|
|
fallback?: ProviderConfig;
|
|
defaults: {
|
|
from: string;
|
|
replyTo?: string;
|
|
};
|
|
}
|
|
|
|
interface ProviderConfig {
|
|
type: 'smtp' | 'resend';
|
|
config: SMTPConfig | ResendConfig;
|
|
}
|
|
|
|
interface SMTPConfig {
|
|
host: string;
|
|
port: number;
|
|
secure: boolean;
|
|
auth: {
|
|
user: string;
|
|
pass: string;
|
|
};
|
|
}
|
|
|
|
interface ResendConfig {
|
|
apiKey: string;
|
|
domain?: string;
|
|
rateLimitPerMinute?: number;
|
|
}
|
|
```
|
|
|
|
### Environment Variables (Centralized Management)
|
|
|
|
```bash
|
|
# Global Email Configuration (Platform Admin Managed)
|
|
EMAIL_ENABLE=true # Existing: Global toggle
|
|
EMAIL_PROVIDER=resend # New: Primary provider (smtp|resend)
|
|
EMAIL_FALLBACK_PROVIDER=smtp # New: Fallback provider
|
|
|
|
# Existing SMTP Configuration (Maintained for Fallback)
|
|
EMAIL_HOST=smtp.gmail.com # Existing: SMTP host
|
|
EMAIL_PORT=587 # Existing: SMTP port
|
|
EMAIL_USERNAME=your-email@gmail.com # Existing: SMTP username
|
|
EMAIL_PASSWORD= # Existing: SMTP password (Docker secret)
|
|
EMAIL_FROM=noreply@yourdomain.com # Existing: Default from address
|
|
|
|
# Centralized Resend Configuration (Platform-Wide)
|
|
RESEND_API_KEY=re_xxxxxxxxx # New: Single Resend account API key (Docker secret)
|
|
RESEND_VERIFIED_DOMAINS=yourdomain.com,tenant1.yourdomain.com # Comma-separated verified domains
|
|
RESEND_DEFAULT_DOMAIN=yourdomain.com # Default domain for tenant subdomains
|
|
RESEND_WEBHOOK_SECRET=whsec_xxxxx # Webhook signature verification secret
|
|
|
|
# Tenant Email Management
|
|
EMAIL_TENANT_SUBDOMAIN_PATTERN="{tenant}.yourdomain.com" # Pattern for tenant subdomains
|
|
EMAIL_TENANT_FROM_PATTERN="noreply@{tenant}.yourdomain.com" # Pattern for tenant from addresses
|
|
|
|
# Backward Compatibility (Legacy Variables)
|
|
SMTP_HOST= # Legacy: Alias for EMAIL_HOST
|
|
SMTP_PORT= # Legacy: Alias for EMAIL_PORT
|
|
SMTP_USER= # Legacy: Alias for EMAIL_USERNAME
|
|
SMTP_PASS= # Legacy: Alias for EMAIL_PASSWORD
|
|
SMTP_FROM= # Legacy: Alias for EMAIL_FROM
|
|
```
|
|
|
|
### Tenant Email Configuration (Database-Driven)
|
|
|
|
Instead of environment variables per tenant, tenant-specific settings are managed in the database:
|
|
|
|
```sql
|
|
-- New table for tenant email settings
|
|
CREATE TABLE tenant_email_settings (
|
|
id SERIAL PRIMARY KEY,
|
|
tenant UUID NOT NULL REFERENCES tenants(tenant),
|
|
from_domain VARCHAR(255) NOT NULL, -- tenant1.yourdomain.com
|
|
default_from_address VARCHAR(255) NOT NULL, -- noreply@tenant1.yourdomain.com
|
|
reply_to_address VARCHAR(255), -- support@tenant1.yourdomain.com
|
|
custom_headers JSONB DEFAULT '{}', -- Tenant-specific headers
|
|
tags TEXT[] DEFAULT '{}', -- Default tags for tenant emails
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(tenant)
|
|
);
|
|
```
|
|
|
|
### Docker Secrets Integration
|
|
```bash
|
|
# Existing Secrets (Maintained)
|
|
email_password # SMTP password
|
|
token_secret_key # Authentication
|
|
nextauth_secret # NextAuth
|
|
|
|
# New Secrets
|
|
resend_api_key # Resend API key
|
|
```
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
- Provider interface compliance
|
|
- Email data validation
|
|
- Configuration validation
|
|
- Error handling scenarios
|
|
|
|
### Integration Tests
|
|
- SMTP provider with test email server
|
|
- Resend provider with sandbox/test addresses
|
|
- Factory pattern provider selection
|
|
- Fallback mechanism testing
|
|
|
|
### End-to-End Tests
|
|
- Complete email sending workflows
|
|
- Template rendering with different providers
|
|
- Attachment handling
|
|
- Error recovery scenarios
|
|
|
|
## Security Considerations
|
|
|
|
1. **API Key Management**: Secure storage of Resend API keys
|
|
2. **Email Validation**: Prevent email injection attacks
|
|
3. **Rate Limiting**: Implement sending rate limits
|
|
4. **Domain Verification**: Ensure proper domain setup for production
|
|
5. **Logging**: Avoid logging sensitive email content
|
|
|
|
## Monitoring & Observability
|
|
|
|
1. **Metrics**: Track sending success rates by provider
|
|
2. **Logging**: Log email sending attempts and results
|
|
3. **Alerting**: Alert on provider failures or high error rates
|
|
4. **Dashboard**: Monitor email queue and delivery statistics
|
|
|
|
## Migration Strategy
|
|
|
|
### Phase 1: Backward Compatible Integration
|
|
1. **Provider Layer Introduction**: Add provider abstraction without changing existing APIs
|
|
2. **Configuration Enhancement**: Extend environment variables while maintaining backward compatibility
|
|
3. **Gradual Service Migration**: Migrate services one at a time starting with least critical
|
|
4. **Monitoring**: Add comprehensive logging for both old and new implementations
|
|
|
|
### Phase 2: Feature Flag Rollout
|
|
1. **Environment-Based Switching**: Use `EMAIL_PROVIDER` to control provider selection
|
|
2. **Tenant-Level Configuration**: Allow per-tenant provider configuration (future enhancement)
|
|
3. **A/B Testing**: Split traffic between providers for performance comparison
|
|
4. **Fallback Validation**: Thoroughly test automatic fallback mechanisms
|
|
|
|
### Phase 3: Service Consolidation
|
|
1. **API Unification**: Consolidate multiple email service implementations
|
|
2. **Legacy Cleanup**: Remove duplicate email service code
|
|
3. **Configuration Simplification**: Streamline environment variable usage
|
|
4. **Documentation Updates**: Update all references to new unified system
|
|
|
|
### Rollback Strategy
|
|
1. **Environment Variable Rollback**: Set `EMAIL_PROVIDER=smtp` to revert to SMTP
|
|
2. **Service-Level Rollback**: Capability to revert individual services to original implementation
|
|
3. **Configuration Isolation**: Keep old and new configurations separate during transition
|
|
4. **Monitoring Alerts**: Automated alerts for email delivery failures or provider issues
|
|
|
|
### Risk Mitigation
|
|
1. **Comprehensive Testing**: Test all email flows before production deployment
|
|
2. **Gradual Deployment**: Deploy to staging environments first, then production
|
|
3. **Monitoring**: Real-time monitoring of email delivery rates and provider health
|
|
4. **Emergency Procedures**: Documented procedures for quick provider switching in emergencies
|
|
|
|
## Future Enhancements
|
|
|
|
1. **Additional Providers**: SendGrid, Mailgun, Amazon SES
|
|
2. **Email Templates**: Enhanced template system with provider-specific optimizations
|
|
3. **Webhooks**: Implement delivery status tracking via provider webhooks
|
|
4. **Email Analytics**: Advanced email performance metrics
|
|
5. **Queue System**: Implement email queue for high-volume sending
|
|
|
|
## Success Criteria
|
|
|
|
- [ ] Both SMTP and Resend providers successfully send emails
|
|
- [ ] Zero downtime migration from existing email service
|
|
- [ ] Improved email deliverability with Resend
|
|
- [ ] Comprehensive test coverage (>90%)
|
|
- [ ] Complete documentation and deployment guides
|
|
- [ ] Monitoring and alerting in place |