One of the most effective means of growing an audience is through email referrals and email marketing strategies. The rise of newsletters like Morning Brew and companies like Harry’s showcase that offering a tiered rewards structure for referring friends can grow a list to astronomical numbers. That is, provided your newsletter or product is actually something people want. The following describes how you can build a simple version of this program yourself using Ruby on Rails API.
At a high level, subscribers will be added to the database when a subscriber is successfully subscribed to our mailchimp list. This allows us to check these against our existing subscribers as well as control sends through our unsubscribe list as well.
Now the call to mailchimp could be done inside our Rails API controller or a model hook, or it could live on our web server and we can make the call to our referral program API in a promise when the mailchimp subscription succeeds. I opted for the latter approach for two reasons:
Upon signing up, subscribers are issued a user_key
and share_key
. The user_key
is a unique key where a subscriber can check their progress toward a reward and find their unique share link that will allow them to receive attribution for referrals. The share_key
is a unique key that gets appended to a subscribers’ referral url that they will share with friends.
When the share_key
of another user is seen in a new subscription that will queue up a validation email. When the link in that validation email is clicked (ensuring it is a real email address) that referral will get attributed to the referee.
When a subscribers referral count reaches five, they are sent an email letting them know they have won a reward and to add their address to have it sent to them. When that subscriber adds their address this triggers an alert to the team to fulfill their reward.
When the reward is fulfilled that user is notified via email that their prize has shipped.
Now for those of you thinking, but what if they have two email addresses?You could just make five unique email addresses to get the prize! This is where business sense has to step in and say, if you are trying to give away a Ferrari, this is not the program for you. If you want a coffee mug or hoodie so bad that you will go through the process of creating five email addresses and referring them, then that is a risk we were willing to accept and reward. In general it’s hard enough to get people to open your newsletters, no less go through all the hoops of gaming the system.
Without further ado let’s get going…
First spin up a rails app in API only mode.
rails new referral_program_api --api
To learn more about why I like using rails for this type of thing check out their documentation: Using Rails for API-only Applications — Ruby on Rails Guides.
And that’s it we now have our API ready to go.
For this we only need to models, one for Subscribers the other for Referrals.
Let’s start by creating the subscribers. We can do so with our rails generator commands.
rails g model subscriber
You can also add all of the attributes to this command as well, however I find it a bit easier to add them to the migration file afterwards, but that’s just a personal preference. So head on over to your /db/migrate/<timestamp>_create_subscribers.rb
file and create the following attributes.
def change
create_table :subscribers do |t|
t.string :email
t.string :first_name
t.string :last_name
t.string :share_key
t.string :user_key
t.string :street_address_one
t.string :street_address_two
t.string :city
t.string :state
t.string :zip
t.boolean :prize_sent
t.boolean :sent_to_fulfillment
t.references :referrer, index: true
t.timestamps
end
end
end
Now we can hop over to our /app/models/subscriber.rb
file where we can setup some rules and callbacks to generate our unique keys for subscribers.
class Subscriber < ApplicationRecord
before_save { email.downcase! }
has_many :referrals, class_name: "Subscriber",
foreign_key: "referrer_id"
validates :first_name, length: { maximum: 50 }
validates :last_name, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 50 }, format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }, uniqueness: {case_sensitive: false}
# Assign a share key
before_create do |subscriber|
subscriber.share_key = generate_unique_key('share_key')
subscriber.user_key = generate_unique_key('user_key')
subscriber.prize_sent = false
subscriber.sent_to_airtable = false
end
private
def generate_unique_key(field_name)
loop do
key = SecureRandom.urlsafe_base64(9).gsub(/-|_/,('a'..'z').to_a[rand(26)])
break key unless Subscriber.exists?("#{field_name}": key)
end
end
end
The first thing we are doing here is converting all email addresses to lowercase and then setting up our association to the referrals model with the line has_many :referrals...
. This is called a self join and will allow us to associate our referrals to a subscriber all in one table.
We then create a few simple validations for first_name
, last_name
, and email_address
.
We then use the before_create
callback to assign a unique share_key
and user_key to the subscriber. These are generated using a private function that makes sure that each of these keys are unique independently. The likelihood that two keys are the same using the SecureRandom.urlsafe_base64(9)
function is highly unlikely, but as a safeguard we do this in a loop and in the case that the key is already used we generate another.
Now that we have a solid subscriber model we can go ahead and create our controller.
Now that we have the proper model in place we can setup our endpoints and controller.
Run the command:
rails g controller subscriber
Now head over to /app/controllers/subscriber_controller.rb and we can create actions for show
, create
, confirm
, and update
.
| Action | Description |
|————|——————|
| Show | For fetching the subscriber information along with associated referrals. |
| Create | For creating a new subscriber, can take in a referrer_key
parameter, which triggers a confirmation email to the subscriber to confirm the email is truly valid. |
| Confirm | For confirming an email address and giving credit to the referrer via referrer_id
field. |
| Update | For updating the subscriber, used for when prize is won for entering address, triggering notifications to the fulfillment team. |
We are not going to be using a delete action here. In general we flag users as unsubscribed in email but we like to never forget them. We don’t have to create any unsubscribe flags in this program since this is still going to be managed by mailchimp and all subscribes and referrals will be passing through their management system before ever hitting our API.
class SubscriberController < ApplicationController
def show
end
def create
end
def confirm
end
def update
end
private
def subscriber_params
params
.require(:subscriber)
.permit(
:first_name,
:last_name,
:email,
:street_address_one,
:street_address_two,
:city,
:state,
:zip,
:referrer_key
)
end
end
We want to be sure to only allow the parameters we have set forth at this stage. You’ll notice that we are not allowing parameters that are not going to be updatable by the user and we have included one additional parameter of referrer_key
, which will have the share_key
of the referring subscriber if this is a referral.
It’s important to remember that all of our subscribers, whether they are referred to us or not need to pass through the referral program to be properly assigned a user_key
and share_key
.
Here we’re going to be adding a few more private function to our controller in order to set the subscriber and referrer prior to our actions. I’m using ... for brevity here, this builds upon the boilerplate laid out above.
class SubscriberController < ApplicationController
before_action :set_subscriber, only: [:show, :update, :confirm]
before_action :set_referrer, only: [:create, :confirm]
...
private
def set_subscriber
@subscriber = Subscriber.find_by_user_key(params[:id])
end
def set_referrer
@referrer = Subscriber.find_by_share_key(params[:referrer_key])
end
...
end
These functions set our subscriber and referrer. We are also adding some before_action
hooks here to set these instance variables in the functions we need them in.
Now that we have our base actions setup we can head over to /config/routes.rb
and define our routes. I like to setup the actions as follows:
Rails.application.routes.draw do
get '/subscriber/:id' => 'subscriber#show'
post '/confirm-subscriber/:id' => 'subscriber#confirm'
post '/subscriber' => 'subscriber#create'
put '/subscriber/:id' => 'subscriber#update'
end
The :id
field in this case is going to be our user_key
so as users can only be looked up by their unique key that will only be sent to their personal emails. Again, we are going to take privacy seriously here and even if a user key was somehow compromised the only thing that we are going to return is the users’ email address, user key, and share key. Sensitive information like address and name are going to be used only or our internal administrators and do not need to be accessible at any endpoint.
Now let’s fill in our actions.
class SubscriberController < ApplicationController
...
def show
render json: @subscriber, :only => ['email', 'user_key', 'share_key', 'sent_to_airtable', 'prize_sent'], :include => { :referrals => { :only => :email } }
end
def create
@subscriber = Subscriber.new(subscriber_params)
if @subscriber.save
if @referrer
# Send email here to confirm the referral email
end
render json: @subscriber, status: :created, location: @subscriber, :only => ['email', 'user_key', 'share_key']
else
render json: @subscriber.errors, status: :unprocessable_entity
end
end
def confirm
if @subscriber && @referrer
@subscriber.update(
referrer_id: @referrer.id
)
if get_referral_count(@referrer.id) == 5
# send reward notification
end
render json: @subscriber, status: :created, location: @subscriber, :only => ['email', 'user_key', 'share_key']
else
render json: { message: 'Bad user key or share key.' }, status: :unprocessable_entity
end
end
def update
if !@subscriber
create
elsif @subscriber.update(subscriber_params)
if get_referral_count(@subscriber.id) >= 5 && has_complete_address?
# Sent to fulfillment service
end
render json: @subscriber, status: :created, location: @subscriber, :only => ['email', 'user_key', 'share_key']
else
render json: @subscriber.errors, status: :unprocessable_entity
end
end
private
...
def get_referral_count(id)
Subscriber.where(referrer_id: id).count
end
def has_complete_address?
@subscriber.street_address_one != nil && @subscriber.city != nil && @subscriber.state != nil && @subscriber.zip != nil
end
...
end
Here, we’ve filled in our controllers except for three key notification points.
To complete these we will need to setup some mailers in a later section.
We also add two more private functions here to get_referral_count
of a referee, and has_complete_address
? to see if a subscriber has entered a complete address.
At this point we have to setup our connections with mailchimp in two places.
user_key
and share_key
back to mailchimp as merge_vars.If you do not want to use mailchimp or gibbon you may skip the next section and integrate whichever email service provider and transactional emails service you are comfortable with.
I find Gibbon to be the best gem for interacting with mailchimp via a rails application. We’ll also be using the figaro gem to allow for environment variables so we don’t commit our mailchimp api keys to source code.
Add the following to your Gemfile
.
# use for environment variables
gem "figaro"
# mailchimp
gem "gibbon", '~>2.2.4'
Then run bundle install
.
Create an application.yml
file in your /config
folder.
touch ./config/application.yml
Then head over to Mailchimp and get your API key and list id. Be advised these are in two different locations inside of Mailchimp. I also like to setup a variable used to signal that we are in the development or staging environment for testing purposes.
MAILCHIMP_API_KEY: '<your_mailchimp_api_key>'
MAILCHIMP_LIST_ID: '<your_mailchimp_list_id>'
MAILCHIMP_ENV: 'development'
Create a gibbon.rb
file inside ./config/initializers
.
touch ./config/initializers/gibbon.rb
Inside this file you will setup the connection to Mailchimp via the Gibbon gem when the rails server starts. Hence, being in the initializers folder.
Gibbon::Request.api_key = ENV["MAILCHIMP_API_KEY"]
Gibbon::Request.timeout = 15
Gibbon::Request.throws_exceptions = false unless ENV["MAILCHIMP_ENV"] == "development"
if ENV["CONTENTFUL_ENV"] == "development"
puts "MailChimp API key: #{Gibbon::Request.api_key}"
end
At this point you can fire up your rails server and you should see the above line printed to the console letting you know our connection to Mailchimp is up and running.
We can now add Gibbon to our subscriber model callbacks in order to send the share_key
and user_key
back to mailchimp. But first, we have to allow for these in our subscriber list.
If you go to your mailchimp audiences tab and select the list we used for the list id above you can click settings and select Audience fields and |MERGE| tags. Here you will be able to add two new fields with the labels Share Key and User Key and the tag names SHAREKEY
and USERKEY
to be used inside templates.
We can now use gibbon to send over the proper values in the after_save
callback.
...
after_save do |subscriber|
unless Rails.env.test?
begin
gibbon = Gibbon::Request.new
gibbon.lists(ENV['MAILCHIMP_LIST_ID']).members(Digest::MD5.hexdigest(subscriber.email))
.update(body: {
merge_fields: {
SHAREKEY: subscriber.share_key,
USERKEY: subscriber.user_key
}
})
rescue Gibbon::MailChimpError => e
# This is a good place to add your error alerting but is beyond the scope of this
puts "Houston, we have a problem: #{e.raw_body}"
end
end
end
...
The key thing here is that if our connection to mailchimp fails for whatever reason, we are still subscribing the user to be corrected later. Inside of the rescue
statement a pro tip would be to put your preferred error alerting system to alert you of an issue sending the keys over to mailchimp without affecting the user experience.
Next we can setup mandrill, mailchimp’s transactional email service. I’m not going to go into how to set this up on the mailchimp side in this tutorial but mailchimp has great help docs and it takes about five minutes to connect mandrill to you mailchimp account.
Inside you rails app you are going to add the mandrill-api
package to your Gemfile.
#mandrill
gem ‘mandrill-api’
Run bundle install
.
Pull up your /config/application.yml
file again and add the following environment variables.
SMTP_ADDRESS: 'smtp.mandrillapp.com'
SMTP_DOMAIN: 'localhost'
SMTP_PASSWORD: '<your_mandrill_smtp_password>'
SMTP_USERNAME: '<your_mandrill_username>'
The password and username can be obtained inside of mandrill for use in this file.
Next we will create our base_mandrill_mailer
that will setup all of our default values for our notification emails to inherit.
Create our base_mandrill_mailer.rb
file:
touch ./app/mailers/base_mandrill_mailer.rb
And add the following:
require "mandrill"
class BaseMandrillMailer < ActionMailer::Base
default(
from: "<your_from_email_address>",
reply_to: "<your_reply_to_email_address>"
)
private
def send_mail(email, subject, body)
mail(to: email, subject: subject, body: body, content_type: "text/html")
end
def mandrill_template(template_name, attributes)
mandrill = Mandrill::API.new(ENV["SMTP_PASSWORD"])
merge_vars = attributes.map do |key, value|
{ name: key, content: value }
end
mandrill.templates.render(template_name, [], merge_vars)["html"]
end
end
Be sure to fill in your from and reply to email values above.
Now we can create two mailers, one for confirmation emails the other for rewards notifications.
touch ./mailers/confirmation_mailer.rb ./mailers/reward_mailer.rb
class ConfirmationMailer < BaseMandrillMailer
def confirm_email(subscriber, referrer_key)
subject = "Confirm Your Email"
merge_vars = {
"USERKEY" => subscriber.user_key,
"REFERRERKEY" => referrer_key,
"EMAIL" => subscriber.email
}
body = mandrill_template("Confirm Email", merge_vars)
send_mail(subscriber.email, subject, body)
end
end
class ConfirmationMailer < BaseMandrillMailer
def confirm_email(subscriber, referrer_key)
subject = "Confirm Your Email"
merge_vars = {
"USERKEY" => subscriber.user_key,
"REFERRERKEY" => referrer_key,
"EMAIL" => subscriber.email
}
body = mandrill_template("Confirm Email", merge_vars)
send_mail(subscriber.email, subject, body)
end
end
class RewardMailer < BaseMandrillMailer
def reward_achieved(subscriber)
subject = "Your Gift Awaits!"
merge_vars = {
"USERKEY" => subscriber.user_key,
"EMAIL" => subscriber.email
}
body = mandrill_template("Reward Achieved", merge_vars)
send_mail(subscriber.email, subject, body)
end
def reward_sent(subscriber)
subject = "Your Gift Is On The Way"
merge_vars = {
"EMAIL" => subscriber.email
}
body = mandrill_template("Reward Sent", merge_vars)
send_mail(subscriber.email, subject, body)
end
def notify_fulfillment_team(subscriber)
subject = "Someone just earned themselves a free tote!"
merge_vars = {
"EMAIL" => subscriber.email
}
body = mandrill_template("Notify Fulfillment Team", merge_vars)
send_mail('<the_email_of_whoever_fulfills_rewards>', subject, body)
end
end
You’ll notice that these mailers reference some templates that are not in mandrill yet. You are correct. What we have to do next is go over to mailchimp and create four templates inside their editor named the following:
Confirm Email
Reward Achieved
Reward Sent
Notify Fulfillment Team
I like to keep them simple and inside each of these templates you need to use your merge_vars
to create links to your front end that will pass the proper keys to your application.
We’ll just be focusing on the API layer since use of merge vars and how this interacts with the front end of the site can vary based upon configurations.
At last we can add our email notifications to our controller actions.
Now we can head back into our subscriber controller and add the following notifications.
class SubscriberController < ApplicationController
...
def create
...
if @referrer
# Send email here to confirm the referral email
ConfirmationMailer.confirm_email(@subscriber, @referrer.share_key).deliver_now
end
...
end
def confirm
...
if get_referral_count(@referrer.id) == 5
# send reward notification
RewardMailer.reward_achieved(@referrer).deliver_now
end
...
end
def update
...
if get_referral_count(@subscriber.id) >= 5 && has_complete_address?
# Sent to fulfillment service
@subscriber.update(sent_to_fulfillment: true)
RewardMailer.notify_fulfillment_team(self).deliver_now
end
...
end
...
end
We now have all that we need to fire up the program. There’s just one mailer that we created that we did not use. That’s the one notifying the user that their order has shipped. For brevity I decided to leave out how you want to manage that since that can vary by what you want to do.
What I decided to do is send the orders over to Airtable and have a field that marks them as pending. I then run a cron job that checks the table each day and if fulfillments are marked as fulfilled since that last run we send an email letting the subscriber know their reward has shipped.
This is the final part of our api. While we don’t want users to have to log in to view their progress and to get their share links, we don’t just want these endpoints accessible to the public.
What we are going to do here is just integrate basic token based authentication with our API, using a key we generate and store in our application.yml
file. On our server this is going to be a part of our configuration file that should not be shared with anyone. When we implemented the figaro gem before this application.yml
file should be a part of our .gitignore
and never checked into source control.
First let’s jump over to our /app/controllers/application_controller.rb
file and add the following.
class ApplicationController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate
protected
def authenticate
authenticate_token || render_unauthorized
end
def authenticate_token
authenticate_with_http_token do |token, options|
token == ENV['SECRET_KEY_BASE']
end
end
def render_unauthorized(realm = "Application")
self.headers["WWW-Authenticate"] = %(Token realm="#{realm}")
render json: 'Bad credentials', status: :unauthorized
end
end
Here we are using Action Controller’s built in http token authentication method. We setup a before_action
hook that ensures that any requested resource in any controller passes through our authenticate
method.
We then create a set of protected
methods, which are similar to private methods except these are accessible by any subclass of our main ApplicationController
class.
In our authenticate_token method we just do a simple comparison to check if the token is equal to the token in our environment variables.
To generate our token I like to just use a variant of our generate_token
function in our controller in the rails console.
Open a new terminal, navigate to your rails project, and run the command rails c
Enter the following in the rails console:
SecureRandom.urlsafe_base64(32).gsub(/-|_/,('a'..'z').to_a[rand(26)])
Anytime you run this you will end up with a string that looks something like this:
c3OcKh1J8a8ich9bQC0TNsdP7LdCqcWL57cJKoch4a0
Take the unique key you have generated and add it to your application.yml
file as SECRET_KEY_BASE
like so:
SECRET_KEY_BASE: 'c3OcKh1J8a8ich9bQC0TNsdP7LdCqcWL57cJKoch4a0'
Now if you try to access a resource you should get an unauthorized response.
In order to get a valid response again you need to pass the header Authorization: “Token token= c3OcKh1J8a8ich9bQC0TNsdP7LdCqcWL57cJKoch4a0”
Make sure that you are always doing this over https
so that this is encrypted. When you build out your front end you must ensure that this key is NOT exposed on the client nor checked into version control like github. If you are using a node based front end, make sure to use a package like DOTENV
which gives you similar functionality to figaro for ruby. You can then create a route in your node server to handle these responses and keeps this key hidden from your front end code.
And that’s all folks. Our API is complete and you can deploy this to heroku or wherever you like to deploy your rails apps to. This is just an API and you can design the front end however you like. As always appreciate any feedback on the approach. Happy coding.