Skip to main content

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.

FieldDescription
Triggercustomer.subscription.created
Template PrefixPREMIUM-WELCOME
PurposeWelcome subscriber, provide password reset link

Goodbye Email

Sent when a subscription is cancelled or expires.

FieldDescription
Triggercustomer.subscription.deleted
Template PrefixPREMIUM-GOODBYE
PurposeAcknowledge 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 email
  • PREMIUM-WELCOME-EN - English welcome email
  • PREMIUM-GOODBYE-FR - French goodbye email
  • PREMIUM-GOODBYE-EN - English goodbye email

Locale Fallback

Supported Locales

LocaleLanguage
ENEnglish (fallback)
ITItalian
FRFrench
ESSpanish
PTPortuguese
PLPolish
ARArabic
SISlovenian

SendGrid Template Setup

Creating Templates

  1. Log into SendGrid Dashboard
  2. Navigate to Email API > Dynamic Templates
  3. Click Create a Dynamic Template
  4. 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

VariableTypeDescription
user_emailstringSubscriber's email address
user_display_namestringSubscriber's full name
new_userbooleanTrue if account was just created
password_generation_linkstringPassword reset URL (only for new users)
plan_namestringDisplay name of the plan
plan_tierstringPlan tier identifier
localestringSubscriber'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

AttemptDelayAction
1ImmediateFirst attempt
230 secondsRetry
32 minutesRetry
410 minutesRetry
5Dead queueAlert team

Common Errors

ErrorCauseResolution
TemplateNotFoundErrorMissing SendGrid templateCreate template with correct name
SendGrid::ErrorAPI errorCheck API key, rate limits
Net::TimeoutErrorNetwork timeoutAutomatic 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:

  1. Go to Activity in SendGrid Dashboard
  2. Filter by template or recipient
  3. View delivery status, opens, clicks

Metrics to Monitor

MetricDescriptionAlert Threshold
Delivery rate% delivered successfully< 95%
Bounce rate% bounced emails> 5%
Open rate% opened (welcome emails)< 30%
Queue depthPending email jobs> 100