Caffeinate logo

Caffeinate

Caffeinate is a drip campaign engine for Ruby on Rails applications.

Caffeinate tries to make creating and managing timed and scheduled email sequences fun. It works alongside ActionMailer and has everything you need to get started and to successfully manage campaigns. It's only dependency is the stack you're already familiar with: Ruby on Rails.

There's a cool demo with all the things included at caffeinate.email. You can view the marketing site source code here.

Do you suffer from ActionMailer tragedies?

If you have anything like this is your codebase, you need Caffeinate:

class User < ApplicationRecord
  after_commit on: :create do
    OnboardingMailer.welcome_to_my_cool_app(self).deliver_later
    OnboardingMailer.some_cool_tips(self).deliver_later(wait: 2.days)
    OnboardingMailer.help_getting_started(self).deliver_later(wait: 3.days)
  end
end
class OnboardingMailer < ActionMailer::Base
  # Send on account creation
  def welcome_to_my_cool_app(user)
    mail(to: user.email, subject: "Welcome to CoolApp!")
  end

  # Send 2 days after the user signs up
  def some_cool_tips(user)
    return if user.unsubscribed_from_onboarding_campaign?

    mail(to: user.email, subject: "Here are some cool tips for MyCoolApp")
  end

  # Sends 3 days after the user signs up and hasn't added a company profile yet
  def help_getting_started(user)
    return if user.unsubscribed_from_onboarding_campaign?
    return if user.onboarding_completed?

    mail(to: user.email, subject: "Do you need help getting started?")
  end
end

What's wrong with this?

  • You're checking state in a mailer
  • The unsubscribe feature is, most likely, tied to a User, which means...
  • It's going to be so fun to scale horizontally

Caffeinate to the rescue

Caffeinate combines a simple scheduling DSL, ActionMailer, and your data models to create scheduled email sequences.

What can you do with drip campaigns?

  • Onboard new customers with cool tips and tricks
  • Remind customers to use your product
  • Nag customers about using your product
  • Reach their spam folder after you fail to handle their unsubscribe request
  • And more!

Onboarding in Caffeinate

In five minutes you can implement this onboarding campaign, and it won't even hijack your entire app!

Install it

Add to Gemfile, run the installer, migrate:

$ bundle add caffeinate
$ rails g caffeinate:install
$ rake db:migrate

Remove that ActionMailer logic

Just delete it. Mailers should be responsible for receiving context and creating a mail object. Nothing more.

The only other change you need to make is the argument that the mailer action receives:

class OnboardingMailer < ActionMailer::Base
  def welcome_to_my_cool_app(mailing)
    @user = mailing.subscriber
    mail(to: @user.email, subject: "Welcome to CoolApp!")
  end

  def some_cool_tips(mailing)
    @user = mailing.subscriber
    mail(to: @user.email, subject: "Here are some cool tips for MyCoolApp")
  end

  def help_getting_started(mailing)
    @user = mailing.subscriber
    mail(to: @user.email, subject: "Do you need help getting started?")
  end
end

While we're there, let's add an unsubscribe link to the views or layout:

<%= link_to "Stop receiving onboarding tips :(", caffeinate_unsubscribe_url %>

Create a Dripper

A Dripper has all the logic for your Campaign and coordinates with ActionMailer on what to send.

In app/drippers/onboarding_dripper.rb:

class OnboardingDripper < ApplicationDripper
  drip :welcome_to_my_cool_app, mailer: 'OnboardingMailer', delay: 0.hours
  drip :some_cool_tips, mailer: 'OnboardingMailer', delay: 2.days
  drip :help_getting_started, mailer: 'OnboardingMailer', delay: 3.days
end

The drip syntax is def drip(mailer_action, options = {}).

Add a subscriber to the Campaign

Call OnboardingDripper.subscribe to subscribe a polymorphic subscriber to the Campaign, which creates a Caffeinate::CampaignSubscription.

class User < ApplicationRecord
  after_commit on: :create do
    OnboardingDripper.subscribe(self)
  end

  after_commit on: :update do
    if onboarding_completed? && onboarding_completed_changed?
      if OnboardingDripper.subscribed?(self)
        OnboardingDripper.unsubscribe(self)
      end
    end
  end
end

When a Caffeinate::CampaignSubscription is created, the relevant Dripper is parsed and Caffeinate::Mailing records are created from the drip DSL. A Caffeinate::Mailing record has a send_at attribute which tells Caffeinate when we can send the mail, which we get from Caffeiate::Mailing#mailer_class and Caffeinate::Mailing#mailer_action.

Run the Dripper

Running OnboardingDripper.perform! every x minutes will call Caffeinate::Mailing#process! on Caffeinate::Mailing records that have send_at < Time.now.

OnboardingDripper.perform!

Done. But wait, there's more fun if you want

  • Automatic subscriptions
  • Campaign-specific unsubscribe links
  • Reasons for unsubscribing so you can have some sort of analytics
  • Periodical emails (daily, weekly, monthly digests, anyone?)
  • Parameterized mailer support a la OnboardingMailer.with(mailing: mailing)

Done. But wait, there's more fun if you want

  • Automatic subscriptions
  • Campaign-specific unsubscribe links
  • Reasons for unsubscribing so you can have some sort of analytics
  • Periodical emails (daily, weekly, monthly digests, anyone?)
  • Parameterized mailer support a la OnboardingMailer.with(mailing: mailing)

Documentation

Upcoming features/todo

Handy dandy roadmap.

Alternatives

Not a fan? There are some alternatives!

Contributing

Just do it.

Contributors & thanks

  • Thanks to sourdoughdev for releasing the gem name to me. :)

License

The gem is available as open source under the terms of the MIT License.