Email Notifications
The Premium system sends transactional emails via SendGrid when subscription events occur. This document covers template configuration, localization, and the email delivery pipeline.
Architecture
Email Types
Welcome Email
Sent when a new premium subscription is created.
| Field | Description |
|---|---|
| Trigger | customer.subscription.created |
| Template Prefix | PREMIUM-WELCOME |
| Purpose | Welcome subscriber, provide password reset link |
Goodbye Email
Sent when a subscription is cancelled or expires.
| Field | Description |
|---|---|
| Trigger | customer.subscription.deleted |
| Template Prefix | PREMIUM-GOODBYE |
| Purpose | Acknowledge cancellation, provide resubscribe info |
Email Flow
SendGrid Service
The Premium::SendgridService handles all email operations.
Public Methods
class Premium::SendgridService
# Send welcome email to new subscriber
def send_welcome_email(email:, dynamic_data:, locale:)
template_id = find_template('PREMIUM-WELCOME', locale)
send_email(email, template_id, dynamic_data)
end
# Send goodbye email when subscription ends
def send_goodbye_email(email:, dynamic_data:, locale:)
template_id = find_template('PREMIUM-GOODBYE', locale)
send_email(email, template_id, dynamic_data)
end
end
Dynamic Data Builder
def dynamic_data(user_result:, plan:)
{
user_email: user_result.email,
user_display_name: user_result.display_name,
new_user: user_result.new_user,
password_generation_link: user_result.password_generation_link,
plan_name: plan.name,
plan_tier: plan.tier,
locale: plan.locale
}
end
Template Configuration
Naming Convention
Templates follow a strict naming pattern:
{PREFIX}-{LOCALE}
Examples:
PREMIUM-WELCOME-IT- Italian welcome emailPREMIUM-WELCOME-EN- English welcome emailPREMIUM-GOODBYE-FR- French goodbye emailPREMIUM-GOODBYE-EN- English goodbye email
Locale Fallback
Supported Locales
| Locale | Language |
|---|---|
EN | English (fallback) |
IT | Italian |
FR | French |
ES | Spanish |
PT | Portuguese |
PL | Polish |
AR | Arabic |
SI | Slovenian |
SendGrid Template Setup
Creating Templates
- Log into SendGrid Dashboard
- Navigate to Email API > Dynamic Templates
- Click Create a Dynamic Template
- Enter template name following naming convention (e.g.,
PREMIUM-WELCOME-IT)
Template Variables
Use Handlebars syntax for dynamic content:
<h1>Welcome, {{user_display_name}}!</h1>
<p>Thank you for subscribing to {{plan_name}}.</p>
{{#if new_user}}
<p>Set up your password:</p>
<a href="{{password_generation_link}}">Create Password</a>
{{/if}}
<p>Your plan: {{plan_tier}}</p>
Available Variables
| Variable | Type | Description |
|---|---|---|
user_email | string | Subscriber's email address |
user_display_name | string | Subscriber's full name |
new_user | boolean | True if account was just created |
password_generation_link | string | Password reset URL (only for new users) |
plan_name | string | Display name of the plan |
plan_tier | string | Plan tier identifier |
locale | string | Subscriber's locale |
Welcome Email Template Example
<!DOCTYPE html>
<html lang="{{locale}}">
<head>
<meta charset="UTF-8">
<title>Welcome to Aleteia Premium</title>
</head>
<body>
<h1>Welcome, {{user_display_name}}!</h1>
<p>Thank you for joining Aleteia Premium with the {{plan_name}} plan.</p>
{{#if new_user}}
<div class="password-section">
<h2>Set Up Your Account</h2>
<p>Click the button below to create your password and access your premium content:</p>
<a href="{{password_generation_link}}" class="button">
Create Password
</a>
<p><small>This link expires in 1 hour.</small></p>
</div>
{{else}}
<div class="existing-user-section">
<p>Your existing account has been upgraded to premium!</p>
<a href="https://aleteia.org/login" class="button">
Sign In
</a>
</div>
{{/if}}
<h2>What's Included</h2>
<ul>
<li>Ad-free reading experience</li>
<li>Exclusive premium content</li>
<li>Early access to new features</li>
</ul>
<p>If you have any questions, contact us at [email protected]</p>
</body>
</html>
Goodbye Email Template Example
<!DOCTYPE html>
<html lang="{{locale}}">
<head>
<meta charset="UTF-8">
<title>We're sorry to see you go</title>
</head>
<body>
<h1>Goodbye, {{user_display_name}}</h1>
<p>Your Aleteia Premium {{plan_name}} subscription has ended.</p>
<p>We hope you enjoyed your premium experience. Here's what you'll miss:</p>
<ul>
<li>Ad-free reading</li>
<li>Exclusive content</li>
<li>Premium features</li>
</ul>
<div class="resubscribe-section">
<h2>Want to come back?</h2>
<p>You can resubscribe anytime:</p>
<a href="https://aleteia.org/premium" class="button">
Resubscribe
</a>
</div>
<p>Thank you for being part of our community.</p>
</body>
</html>
Template Caching
Templates are cached to reduce API calls:
class Premium::SendgridService
TEMPLATE_CACHE_TTL = 1.hour
def find_template(prefix, locale)
cache_key = "sendgrid_template:#{prefix}:#{locale}"
Rails.cache.fetch(cache_key, expires_in: TEMPLATE_CACHE_TTL) do
search_template("#{prefix}-#{locale.upcase}") ||
search_template("#{prefix}-EN") ||
raise(TemplateNotFoundError, "No template found for #{prefix}")
end
end
end
Cache Invalidation
Clear the cache when updating templates:
# Rails console
Rails.cache.delete_matched("sendgrid_template:*")
Background Job
Premium::SendEmailJob
class Premium::SendEmailJob < ApplicationJob
queue_as :email
def perform(email_type, user_data, plan_data)
user_result = UserResult.new(**user_data.symbolize_keys)
plan = Plan.new(**plan_data.symbolize_keys)
service = Premium::SendgridService.new
dynamic_data = service.dynamic_data(user_result: user_result, plan: plan)
case email_type
when 'welcome'
service.send_welcome_email(
email: user_result.email,
dynamic_data: dynamic_data,
locale: plan.locale
)
when 'goodbye'
service.send_goodbye_email(
email: user_result.email,
dynamic_data: dynamic_data,
locale: plan.locale
)
end
end
end
Queue Configuration
Emails use a dedicated queue for priority and monitoring:
# config/sidekiq.yml
:queues:
- [critical, 3]
- [default, 2]
- [email, 2]
- [low, 1]
Error Handling
Retry Strategy
| Attempt | Delay | Action |
|---|---|---|
| 1 | Immediate | First attempt |
| 2 | 30 seconds | Retry |
| 3 | 2 minutes | Retry |
| 4 | 10 minutes | Retry |
| 5 | Dead queue | Alert team |
Common Errors
| Error | Cause | Resolution |
|---|---|---|
TemplateNotFoundError | Missing SendGrid template | Create template with correct name |
SendGrid::Error | API error | Check API key, rate limits |
Net::TimeoutError | Network timeout | Automatic retry |
Testing
RSpec Examples
describe Premium::SendgridService do
describe '#send_welcome_email' do
it 'sends email with correct template' do
expect(sendgrid_client).to receive(:send).with(
hash_including(
template_id: 'd-abc123',
personalizations: [
hash_including(
to: [{ email: '[email protected]' }],
dynamic_template_data: hash_including(
user_display_name: 'John Doe',
new_user: true
)
)
]
)
)
service.send_welcome_email(
email: '[email protected]',
dynamic_data: { user_display_name: 'John Doe', new_user: true },
locale: 'en'
)
end
end
end
VCR Cassettes
HTTP interactions are recorded for testing:
describe Premium::SendgridService, :vcr do
it 'finds template by locale' do
VCR.use_cassette('sendgrid/find_template_it') do
template_id = service.find_template('PREMIUM-WELCOME', 'it')
expect(template_id).to eq('d-xyz789')
end
end
end
Monitoring
Logging
logger.info("Email sent",
email_type: 'welcome',
recipient: email,
template_id: template_id,
locale: locale,
message_id: response.headers['x-message-id']
)
SendGrid Activity Feed
Monitor email delivery in SendGrid:
- Go to Activity in SendGrid Dashboard
- Filter by template or recipient
- View delivery status, opens, clicks
Metrics to Monitor
| Metric | Description | Alert Threshold |
|---|---|---|
| Delivery rate | % delivered successfully | < 95% |
| Bounce rate | % bounced emails | > 5% |
| Open rate | % opened (welcome emails) | < 30% |
| Queue depth | Pending email jobs | > 100 |
Related Documentation
- Overview - System architecture overview
- Setup Guide - SendGrid configuration
- Stripe Integration - Event triggers for emails