Files
lunarfront-app/planning/24_Email_System.md
Ryan Moon 5aadd68128 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.
2026-03-28 15:38:54 -05:00

11 KiB

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

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