alumni_lookup

External Services Environment Configuration

Last Updated: December 2, 2025
Purpose: Define how external services are configured per environment to ensure safety and proper isolation.


Table of Contents

  1. Service Matrix Overview
  2. Email (Mailgun)
  3. File Storage (Cloudinary)
  4. CRM Integration (Salesforce)
  5. Other Integrations
  6. Safe Defaults
  7. Environment Variable Reference

Service Matrix Overview

Service Development Test Staging Production
Email Letter Opener Test adapter Mailgun Sandbox Mailgun Live
Storage Local disk Temp disk Cloudinary (staging folder) Cloudinary
CRM Disabled Mocked Disabled Export-only
Logging File STDERR Heroku Logs Heroku Logs + optional APM

Email (Mailgun)

Development

Setting Value
Delivery Method :letter_opener
Behavior Opens emails in browser (gem: letter_opener)
Network Calls None
Configuration config/environments/development.rb
# development.rb
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true

No environment variables required.


Test

Setting Value
Delivery Method :test
Behavior Emails stored in ActionMailer::Base.deliveries
Network Calls None
Configuration config/environments/test.rb
# test.rb
config.action_mailer.delivery_method = :test

Testing emails in assertions:

assert_emails 1 do
  ChampionSignupMailer.admin_notification(signup).deliver_now
end
email = ActionMailer::Base.deliveries.last
assert_equal ['admin@example.com'], email.to

Staging

Setting Value
Delivery Method :mailgun_api (same as production)
Behavior Sandbox mode OR restricted recipients
Network Calls Yes, to Mailgun API
Configuration Heroku config vars

Option A: Mailgun Sandbox Domain

Use Mailgun’s sandbox domain which only delivers to verified recipients:

# Heroku config vars for staging
MAILGUN_API_KEY=key-xxx
MAILGUN_DOMAIN=sandboxXXX.mailgun.org  # Sandbox domain
MAILGUN_API_HOST=api.mailgun.net

Option B: Recipient Whitelist (Code Change)

Add a staging guard in the mailer:

# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  before_action :restrict_recipients_in_staging

  private

  def restrict_recipients_in_staging
    return unless Rails.env.staging?
    
    allowed = ENV.fetch('STAGING_EMAIL_WHITELIST', '').split(',')
    mail.to = mail.to.select { |email| allowed.include?(email) }
  end
end

Recommendation: Use Option A (Sandbox Domain) — simpler, no code changes.


Production

Setting Value
Delivery Method :mailgun_api
Behavior Live email delivery
Network Calls Yes, to Mailgun API
Configuration Heroku config vars + custom delivery class

Required Environment Variables:

Variable Description Example
MAILGUN_API_KEY Mailgun API key key-xxxxxxxxxxxxxxxx
MAILGUN_DOMAIN Verified sending domain mg.bualum.co
MAILGUN_API_HOST API endpoint api.mailgun.net

Current Implementation:


File Storage (Cloudinary)

Development

Setting Value
Storage Service :local
Location storage/ directory
Network Calls None
Configuration config/environments/development.rb
# development.rb
config.active_storage.service = :local

Test

Setting Value
Storage Service :test
Location tmp/storage/ (ephemeral)
Network Calls None
Configuration config/environments/test.rb
# test.rb
config.active_storage.service = :test

Staging

Setting Value
Storage Service :cloudinary
Behavior Real Cloudinary, isolated folder
Network Calls Yes, to Cloudinary API
Configuration Heroku config vars

Option A: Same Cloudinary Account, Different Folder

Use Cloudinary’s folder feature to namespace staging uploads:

# Could add to an initializer or Active Storage config
# Cloudinary automatically uses Rails.env in paths

Option B: Separate Cloudinary Account

Create a free Cloudinary account for staging (25GB free).

Required Environment Variable:

Variable Description Example
CLOUDINARY_URL Full Cloudinary URL cloudinary://api_key:api_secret@cloud_name

Recommendation: Use same account with Rails.env namespacing — simpler, free tier usually sufficient.


Production

Setting Value
Storage Service :cloudinary
Behavior Live file storage
Network Calls Yes, to Cloudinary API
Configuration Heroku config vars
# production.rb
config.active_storage.service = :cloudinary

Required Environment Variable:

Variable Description
CLOUDINARY_URL Production Cloudinary credentials

CRM Integration (Salesforce)

Current State

The application does NOT have live Salesforce integration. CRM integration is export-only via the contact_id field (format: C-000000000).

Feature Description
Contact ID Import CSV import includes Contact ID for CRM mapping
CSV Export Exports include AdvRM - Contact ID column
Live API Calls None — no Salesforce API integration exists

All Environments

Environment CRM Behavior
Development Contact ID field exists, no network calls
Test Contact ID field exists, no network calls
Staging Contact ID field exists, no network calls
Production Contact ID field exists, no network calls

Future Considerations

If live Salesforce integration is added:

  1. Development/Test: Use environment flag to disable API calls
  2. Staging: Use Salesforce Sandbox org
  3. Production: Live Salesforce org
# Example pattern for future CRM integration
class SalesforceService
  def sync_contact(alumni)
    return if Rails.env.development? || Rails.env.test?
    return if ENV['SALESFORCE_DISABLED'] == 'true'
    
    # API call logic
  end
end

Other Integrations

Caching

Environment Cache Store Configuration
Development :null_store (or :memory_store if enabled) rails dev:cache to toggle
Test :null_store Always disabled
Staging :memory_store Match production
Production :memory_store (64MB) config/environments/production.rb

Session Storage

Environment Session Store  
All Cookie store Default Rails behavior

See config/initializers/session_store.rb if customized.

Background Jobs

Environment Job Adapter Redis Required
Development Sidekiq Yes (local)
Staging Sidekiq Yes (Heroku Redis)
Production Sidekiq Yes (Heroku Redis)

Development Setup

# Install Redis
brew install redis
brew services start redis

# Start with foreman (Sidekiq worker starts automatically)
bin/dev

Heroku Setup (Staging/Production)

# Add Redis addon (if not already added)
heroku addons:create heroku-redis:mini --app your-app-name

# Scale the worker dyno UP
heroku ps:scale worker=1 --app your-app-name

# Scale the worker dyno DOWN (to save costs when not needed)
heroku ps:scale worker=0 --app your-app-name

The REDIS_URL environment variable is automatically set by Heroku when you add the Redis addon.

Verify Sidekiq is Running

# Development: Check Redis
redis-cli ping  # Should return PONG

# Heroku: Check worker status
heroku ps --app your-app-name  # Should show worker=1

Safe Defaults

Fail-Fast vs Silent No-Op

Service Missing Config Behavior Rationale
Mailgun (Prod) Fail fast (raise error) Critical — emails must work
Mailgun (Staging) Fail fast Should match production behavior
Cloudinary (Prod) Fail fast Critical — photos must work
Cloudinary (Staging) Fail fast Should match production
CRM/Salesforce Silent no-op Export-only, not critical path

Guardrails

1. Email Guardrails

Current (in mailgun_delivery.rb):

unless api_key && domain
  Rails.logger.error "[Mailgun] Missing MAILGUN_API_KEY or MAILGUN_DOMAIN"
  raise "Mailgun configuration missing"
end

✅ This is correct — fail fast in production.

Additional safeguard (optional, for staging):

Add recipient filtering to prevent accidental real-user emails:

# Add to ApplicationMailer or create staging interceptor
if Rails.env.staging?
  class StagingEmailInterceptor
    ALLOWED_DOMAINS = %w[@bualum.co @example.com].freeze

    def self.delivering_email(message)
      message.to = message.to&.select do |email|
        ALLOWED_DOMAINS.any? { |domain| email.end_with?(domain) }
      end
      message.cc = []
      message.bcc = []
    end
  end

  ActionMailer::Base.register_interceptor(StagingEmailInterceptor)
end

2. Storage Guardrails

Cloudinary uses the environment in paths by default. No additional guardrails needed.

3. CRM Guardrails

Current implementation is export-only (CSV), so no API guardrails needed.

If live integration is added, use:

SALESFORCE_DISABLED=true  # Staging/dev

Environment Variable Reference

Full List by Environment

Development

Variable Required Value
None for email Uses letter_opener
None for storage Uses local disk

Test

Variable Required Value
None All services use test adapters

Staging

Variable Required Value
RAILS_ENV staging (if using custom env) or production
RAILS_MASTER_KEY Master key for credentials
DATABASE_URL Heroku auto-sets
MAILGUN_API_KEY Staging/sandbox Mailgun key
MAILGUN_DOMAIN Sandbox domain or staging domain
MAILGUN_API_HOST api.mailgun.net (default)
CLOUDINARY_URL Cloudinary credentials
SECRET_KEY_BASE Heroku auto-generates

Production

Variable Required Value
RAILS_ENV production
RAILS_MASTER_KEY Master key for credentials
DATABASE_URL Heroku auto-sets
MAILGUN_API_KEY Production Mailgun key
MAILGUN_DOMAIN mg.bualum.co or verified domain
MAILGUN_API_HOST api.mailgun.net (default)
CLOUDINARY_URL Production Cloudinary credentials
SECRET_KEY_BASE Heroku auto-generates
RAILS_MAX_THREADS 3 (recommended)
WEB_CONCURRENCY 1 (recommended for basic dynos)
RAILS_LOG_LEVEL info (default)

Configuration Checklist

Staging Setup

Production Verification