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