Add email system planning doc for transactional and mass email
Provider abstraction (SMTP, SendGrid, SES, Postmark), email templates with merge fields, BullMQ queue for async sending, mass email campaigns with recipient filtering, CAN-SPAM unsubscribe management, email logging.
This commit is contained in:
327
planning/24_Email_System.md
Normal file
327
planning/24_Email_System.md
Normal file
@@ -0,0 +1,327 @@
|
||||
Music Store Management Platform
|
||||
|
||||
Email System — Transactional & Mass Email
|
||||
|
||||
Version 1.0 | Draft
|
||||
|
||||
|
||||
|
||||
# 1. Overview
|
||||
|
||||
The email system handles all outbound email from the platform — both automated transactional messages (receipts, reminders, notifications) and bulk mass emails (announcements, promotions, newsletters). Like file storage, the email provider is abstracted behind an interface to support multiple backends.
|
||||
|
||||
|
||||
|
||||
# 2. Email Provider Interface
|
||||
|
||||
## 2.1 Providers
|
||||
|
||||
Provider | Backend | Best For
|
||||
smtp | Any SMTP server | Self-hosted stores, existing email infrastructure
|
||||
sendgrid | SendGrid API | Deliverability, templates, analytics
|
||||
ses | AWS SES | Cost-effective high volume, SaaS deployment
|
||||
postmark | Postmark API | Transactional focus, fast delivery
|
||||
|
||||
## 2.2 Configuration
|
||||
|
||||
Variable | Description | Default
|
||||
EMAIL_PROVIDER | smtp, sendgrid, ses, postmark | smtp
|
||||
EMAIL_FROM_ADDRESS | Default sender address | noreply@{company domain}
|
||||
EMAIL_FROM_NAME | Default sender name | {Company Name}
|
||||
SMTP_HOST | SMTP server hostname | (required if smtp)
|
||||
SMTP_PORT | SMTP port | 587
|
||||
SMTP_USER | SMTP username | (required if smtp)
|
||||
SMTP_PASS | SMTP password | (required if smtp)
|
||||
SMTP_SECURE | Use TLS | true
|
||||
SENDGRID_API_KEY | SendGrid API key | (required if sendgrid)
|
||||
AWS_SES_REGION | SES region | (required if ses)
|
||||
AWS_SES_ACCESS_KEY | SES access key | (required if ses)
|
||||
AWS_SES_SECRET_KEY | SES secret key | (required if ses)
|
||||
POSTMARK_API_KEY | Postmark server token | (required if postmark)
|
||||
|
||||
## 2.3 Interface
|
||||
|
||||
```typescript
|
||||
interface EmailProvider {
|
||||
send(message: EmailMessage): Promise<EmailResult>
|
||||
sendBatch(messages: EmailMessage[]): Promise<EmailResult[]>
|
||||
}
|
||||
|
||||
interface EmailMessage {
|
||||
to: string | string[]
|
||||
from?: string // overrides default
|
||||
replyTo?: string
|
||||
subject: string
|
||||
html: string
|
||||
text?: string // plain text fallback
|
||||
tags?: string[] // for tracking/categorization
|
||||
metadata?: Record<string, string> // provider-specific metadata
|
||||
}
|
||||
|
||||
interface EmailResult {
|
||||
success: boolean
|
||||
messageId?: string
|
||||
error?: string
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
# 3. Email Types
|
||||
|
||||
## 3.1 Transactional (System-Triggered)
|
||||
|
||||
These fire automatically based on business events. Each has a template and trigger condition.
|
||||
|
||||
Email | Trigger | Recipients | Template
|
||||
Receipt | Sale completed | Account email | Transaction summary, line items, payment method
|
||||
Rental Agreement | Rental activated | Account email | Agreement PDF attached, signing link
|
||||
Rental Reminder | 3 days before billing | Account email | Upcoming charge, amount, payment method
|
||||
Payment Failed | Charge declined | Account email | Failed amount, retry schedule, update card link
|
||||
Payment Received | Recurring charge success | Account email | Amount, date, next billing date
|
||||
Repair Ready | Repair status → ready | Account email or customer phone | Instrument ready for pickup
|
||||
Repair Estimate | Estimate created | Account email | Estimate details, approve/decline link
|
||||
Lesson Reminder | 24 hours before lesson | Member email or account email | Time, instructor, location
|
||||
Lesson Cancelled | Lesson cancelled | Member email | Reason, reschedule options
|
||||
Tax Exemption Expiring | 30 days before expiry | Account email | Certificate expiry warning
|
||||
Maintenance Due | Maintenance reminder due | Account email | Instrument, service needed, schedule link
|
||||
Welcome | Account created | Account email | Getting started, portal access
|
||||
Password Reset | Reset requested | User email | Reset link (24h expiry)
|
||||
|
||||
## 3.2 Mass Email (Staff-Initiated)
|
||||
|
||||
Staff can send bulk emails to filtered recipient lists. These are NOT marketing automation — they're one-off announcements.
|
||||
|
||||
Use Cases:
|
||||
- Store sale or promotion announcement
|
||||
- Schedule change notification (holiday hours)
|
||||
- Rental program updates
|
||||
- Back-to-school instrument availability
|
||||
- Concert or event invitations
|
||||
- Policy changes
|
||||
|
||||
## 3.3 Recipient Lists
|
||||
|
||||
Mass emails target filtered lists built from account/member data:
|
||||
|
||||
Filter | Description
|
||||
All accounts | Every account with an email
|
||||
Active rentals | Accounts with active rental contracts
|
||||
Active lessons | Members with active lesson enrollments
|
||||
By instrument type | Members renting/owning specific instruments
|
||||
By school | Accounts linked to a specific school
|
||||
By category | Accounts that purchased from a product category
|
||||
By location | Accounts at a specific store location
|
||||
Tax exempt | Tax-exempt accounts only
|
||||
Custom | Staff-built filter with multiple criteria
|
||||
|
||||
|
||||
|
||||
# 4. Database Schema
|
||||
|
||||
## 4.1 email_template
|
||||
|
||||
Store-customizable email templates. System provides defaults, stores can modify.
|
||||
|
||||
Column | Type | Notes
|
||||
id | uuid PK |
|
||||
company_id | uuid FK | Tenant scoping
|
||||
slug | varchar | Unique identifier: receipt, rental_reminder, repair_ready, etc.
|
||||
name | varchar | Display name
|
||||
subject | varchar | Email subject line (supports merge fields)
|
||||
html_body | text | HTML body (supports merge fields)
|
||||
text_body | text | Plain text fallback
|
||||
is_system | boolean | Default false. System templates seeded on company creation.
|
||||
is_active | boolean | Inactive templates don't send
|
||||
created_at | timestamptz |
|
||||
updated_at | timestamptz |
|
||||
|
||||
## 4.2 email_log
|
||||
|
||||
Append-only log of every email sent. Used for troubleshooting, compliance, and deliverability monitoring.
|
||||
|
||||
Column | Type | Notes
|
||||
id | uuid PK |
|
||||
company_id | uuid FK |
|
||||
template_id | uuid FK | Nullable — mass emails may not use a template
|
||||
to_address | varchar | Recipient email
|
||||
from_address | varchar | Sender address used
|
||||
subject | varchar | Actual subject sent
|
||||
message_id | varchar | Provider's message ID for tracking
|
||||
status | varchar | sent, failed, bounced, delivered, opened, clicked
|
||||
error | text | Error message if failed
|
||||
entity_type | varchar | Nullable — what triggered this: transaction, rental, repair_ticket, etc.
|
||||
entity_id | uuid | Nullable — ID of the triggering record
|
||||
campaign_id | uuid FK | Nullable — if part of a mass email campaign
|
||||
sent_at | timestamptz |
|
||||
created_at | timestamptz |
|
||||
|
||||
## 4.3 email_campaign
|
||||
|
||||
Mass email campaigns.
|
||||
|
||||
Column | Type | Notes
|
||||
id | uuid PK |
|
||||
company_id | uuid FK |
|
||||
name | varchar | Campaign name for internal reference
|
||||
subject | varchar | Email subject
|
||||
html_body | text | Email content
|
||||
text_body | text | Plain text fallback
|
||||
status | varchar | draft, scheduled, sending, sent, cancelled
|
||||
filter_criteria | jsonb | Recipient filter criteria (serialized query)
|
||||
recipient_count | integer | Total recipients at send time
|
||||
sent_count | integer | Successfully sent
|
||||
failed_count | integer | Failed to send
|
||||
scheduled_at | timestamptz | Nullable — when to send (null = send immediately)
|
||||
sent_at | timestamptz | When sending started
|
||||
completed_at | timestamptz | When sending finished
|
||||
created_by | uuid FK | Employee who created
|
||||
created_at | timestamptz |
|
||||
updated_at | timestamptz |
|
||||
|
||||
## 4.4 email_unsubscribe
|
||||
|
||||
Opt-out tracking. Respects CAN-SPAM.
|
||||
|
||||
Column | Type | Notes
|
||||
id | uuid PK |
|
||||
company_id | uuid FK |
|
||||
email | varchar | The unsubscribed email address
|
||||
account_id | uuid FK | Nullable
|
||||
reason | text | Nullable — why they unsubscribed
|
||||
unsubscribed_at | timestamptz |
|
||||
created_at | timestamptz |
|
||||
|
||||
|
||||
|
||||
# 5. Merge Fields
|
||||
|
||||
Templates support merge fields that resolve at send time:
|
||||
|
||||
Field | Resolves To
|
||||
{{account_name}} | account.name
|
||||
{{account_email}} | account.email
|
||||
{{member_name}} | member.firstName + member.lastName
|
||||
{{company_name}} | company.name
|
||||
{{company_phone}} | company.phone
|
||||
{{company_email}} | company.email
|
||||
{{amount}} | Transaction/payment amount (formatted)
|
||||
{{date}} | Relevant date (formatted)
|
||||
{{instrument}} | Instrument description
|
||||
{{portal_url}} | Customer portal URL
|
||||
{{unsubscribe_url}} | One-click unsubscribe link (required for mass email)
|
||||
|
||||
Custom merge fields per template type (e.g. repair estimate has {{estimate_total}}, {{repair_description}}).
|
||||
|
||||
|
||||
|
||||
# 6. Email Queue
|
||||
|
||||
Emails are not sent synchronously from API routes. They're queued via BullMQ and processed by a worker.
|
||||
|
||||
## 6.1 Queue Flow
|
||||
|
||||
1. Business event fires (e.g. repair status changes to "ready")
|
||||
2. Event handler creates an email job: `emailQueue.add('send', { templateSlug, recipientAccountId, entityType, entityId, data })`
|
||||
3. Worker picks up the job
|
||||
4. Worker resolves template + merge fields
|
||||
5. Worker calls `emailProvider.send()`
|
||||
6. Result logged to email_log
|
||||
7. On failure: retry with exponential backoff (3 attempts over 1 hour)
|
||||
|
||||
## 6.2 Mass Email Flow
|
||||
|
||||
1. Staff creates campaign with content and filter criteria
|
||||
2. Staff clicks "Send" (or schedules)
|
||||
3. System resolves recipient list from filter criteria
|
||||
4. Creates one job per recipient (batched, throttled)
|
||||
5. Worker processes queue with rate limiting (avoid provider throttling)
|
||||
6. Campaign status updated as batches complete
|
||||
7. Unsubscribed addresses skipped automatically
|
||||
|
||||
## 6.3 Rate Limiting
|
||||
|
||||
Provider | Default Rate
|
||||
smtp | 10/second
|
||||
sendgrid | 100/second
|
||||
ses | 14/second (sandbox), higher with production access
|
||||
postmark | 50/second
|
||||
|
||||
|
||||
|
||||
# 7. Unsubscribe Management
|
||||
|
||||
## 7.1 Requirements (CAN-SPAM)
|
||||
|
||||
- Every mass email includes an unsubscribe link
|
||||
- Unsubscribe is one-click (no login required)
|
||||
- Unsubscribe processed within 10 business days (we do it instantly)
|
||||
- Unsubscribed addresses never receive mass email
|
||||
- Transactional emails (receipts, billing) are exempt from unsubscribe
|
||||
|
||||
## 7.2 Unsubscribe Endpoint
|
||||
|
||||
GET /unsubscribe?token={signed_token}
|
||||
|
||||
- Token encodes email + company_id, signed with JWT secret
|
||||
- No authentication required
|
||||
- Shows confirmation page
|
||||
- Inserts into email_unsubscribe table
|
||||
- Returns "You've been unsubscribed" page
|
||||
|
||||
|
||||
|
||||
# 8. Admin UI
|
||||
|
||||
## 8.1 Email Templates Page
|
||||
|
||||
- List all templates with status (active/inactive)
|
||||
- Edit template: subject, HTML body, text body
|
||||
- Preview with sample data
|
||||
- Reset to system default
|
||||
|
||||
## 8.2 Mass Email Page
|
||||
|
||||
- Create campaign: subject, body (rich text editor), recipient filter
|
||||
- Preview recipient count before sending
|
||||
- Schedule or send immediately
|
||||
- Campaign history with sent/failed counts
|
||||
- View individual send status
|
||||
|
||||
## 8.3 Email Log
|
||||
|
||||
- Searchable log of all sent emails
|
||||
- Filter by template, status, date, recipient
|
||||
- View email content as sent
|
||||
- Resend failed emails
|
||||
|
||||
|
||||
|
||||
# 9. Implementation Order
|
||||
|
||||
1. EmailProvider interface with SmtpProvider
|
||||
2. email_template table + seed system templates
|
||||
3. email_log table
|
||||
4. BullMQ email queue + worker
|
||||
5. Transactional triggers (start with receipt + repair ready)
|
||||
6. Template editor in admin UI
|
||||
7. email_campaign + email_unsubscribe tables
|
||||
8. Mass email UI (create, filter, send)
|
||||
9. Additional providers (SendGrid, SES, Postmark)
|
||||
10. Email log viewer in admin UI
|
||||
|
||||
|
||||
|
||||
# 10. Business Rules
|
||||
|
||||
- Emails are always queued, never sent synchronously from API routes
|
||||
- Failed emails retry 3 times with exponential backoff
|
||||
- Unsubscribed addresses are never sent mass email — checked at send time
|
||||
- Transactional emails always send regardless of unsubscribe status
|
||||
- All emails logged with full content for compliance
|
||||
- Email templates are company-scoped — each store customizes their own
|
||||
- Mass emails require at least one unsubscribe link in the body
|
||||
- Email provider is set at startup via env var — same as storage provider
|
||||
- BullMQ uses the same Valkey instance as the rest of the application
|
||||
- Mass email campaigns are rate-limited per provider to avoid throttling
|
||||
- Empty recipient lists block campaign send with clear error
|
||||
Reference in New Issue
Block a user