Referral System in Rails applications

By Exequiel Rozas

Building a great product is a good starting point for success, but it's seldom enough to generate meaningful traction. There are many channels we can use for distribution: organic search, social media, paid advertising, and PR, among others.

However, if we find success with any of these channels, user acquisition costs usually get higher with time, and that can become a real issue if we're caught off-guard. Adding a referral system is a good way to acquire a portion of our users for a lower price.

In this article, we will learn how to add a referral system to a Rails app using the Refer gem to improve our chances of success.

Let's start by understanding why adding a referral system is desired. Jump to the application setup if you already know about them.

Why add a referral system

The main reason to add a referral system to any product is to reduce customer acquisition costs.

It doesn't matter what our main acquisition channel is, as we go through our product adoption lifecycle, the probability of customer acquisition getting more expensive is high.

Our early customers are more inclined to respond to our early marketing efforts than users who don't: they are already aware of the issues we solve, and they are maybe even aware of our product.

And, while a referral system doesn't solve the customer acquisition costs alone, it gives users an opportunity to recommend our product to people that might be interested while simultaneously earning a reward.

Considering this, a good referral program is:

  • A virtuous cycle: users should preferably refer users that might actually benefit from our product.
  • Attractive but low cost: the reward users get for referring other users should be attractive to them while at the same time not breaking the bank for us.
  • Frictionless: every inch of friction we add to our referral program is going to worsen our results. We want to give users the ability to refer other users as simply as possible.

Focusing on a referral program before validating an idea is probably not a good use of time. But don't sleep on them because the cost/reward ratio can be very high.

How does it work

For a referral system to work, we need to be able to track who referred whom reliably.

In a web application, the best way we have to achieve this is to use cookies, which are a semi-permanent, device-contained storage solution.

To achieve a referral, a user has to share a link that includes a given code with an acquaintance who visits our site. Because the link contains the referral code as a query parameter, we store that code in a cookie in the referred user's browser.

Referral system explained

Then, as the referee has the referrer's code stored in the browser, after completing a signup our application reads the code from the cookies, looks for the referring user, and tracks the referral by persisting it to the database and associating it with the user who owns the code.

Referral flow grant reward

The last step of the process is to provide a reward to the referrer after the referee performs an action in our application.

This step is highly dependent on the business goals. Maybe we want to encourage account creation, or maybe we want to provide the reward after the referee purchases our product, starts a free trial, or does something else of interest for the business.

What we will build

For this tutorial, we will build upon AvoCasts, the fantasy application we built for the multistep forms with Wicked tutorial, by adding a referral system.

We will be using the Refer gem, and our system will have the following features:

  • A user can refer other users by sharing a link.
  • A successful referral is produced when a new user creates an account. Normally, we may want the referral to be successful after a transaction or business goal is produced.
  • The referral should have an attribution window. If a set amount of time passes after a user was referred, we won't count the referral.
  • A given user can only refer a limited amount of users. For our case, let's say it's 5.
  • We will give a reward to both users only after the referred user completes an action.
  • Users can access their referral stats in a dashboard within the application.
  • Stakeholders can access referral stats in a dashboard.
  • Referring should be frictionless: the referral view should include features to copy and share the referral codes. ## Application setup We will assume we have an existing application that we want to add a referral system to.

If you already have auth setup, skip this step. Otherwise, we will add it with the Rails auth generator:

bin/rails generate authentication

This will add two database-backed models: User and Session. We will add the referral associations to the user.

Next, let's add the refer gem to our Gemfile and install it:

bundle add refer && bundle install

With the gem installed, let's take a look at how it works and what it does behind the scenes:

The Refer gem

This command adds three migrations that add the following database tables:

Refer gem database structure

The refer_referrals table is responsible for keeping track of who referred whom: it associates a referrer with a referee. For our example application, they would both be users, but because the association is polymorphic, we could have a User as the referrer and an Author as the referee.

The refer_referral_codes keeps track of the codes that the referrer has. Even though it's common for a user to have a single referral code, sometimes we need to have many to track the origin of the referral. For example, the user might want to see which channel performs better for a referral campaign.

The refer_visits table holds information about the visits that had a code query parameter. They can be useful to analyze why a specific code was successful or not.

The command also adds a has_referrals call to the User model, which is the same as including the Refer::Model concern, which adds the following associations to the model where it's included:

has_many :referral_codes, as: :referrer, class_name: "Refer::ReferralCode", dependent: :destroy
has_many :referrals, as: :referrer, class_name: "Refer::Referral", dependent: :destroy
has_one :referral, as: :referee, class_name: "Refer::Referral", dependent: :destroy
delegate :referrer, to: :referral, allow_nil: true

Referral cookie

The method in charge of adding the referral cookie to the referee's browser is set_referral_cookie .

This method is added to the ApplicationController when installing the gem and is a wrapper around the set_refer_cookie method in a before action:

class_methods do
  def set_referral_cookie(param_name: Refer.param_name, cookie_name: Refer.cookie_name, **options)
    before_action -> { set_refer_cookie(param_name: param_name, cookie_name: cookie_name) }, **options
  end
end

We can customize the name of the parameter, which is set to code by default, the name of the cookie and whether we want to track the visits that are associated with the code:

def set_refer_cookie(param_name: Refer.param_name, cookie_name: Refer.cookie_name, code: nil, track_visit: Refer.track_visits)
  code ||= params[param_name]
  return if code.blank?

  cookies[cookie_name] = Refer.cookie(code) if Refer.overwrite_cookie || cookies[cookie_name].blank?
  ReferralCode.find_by(code: code)&.track_visit(request) if track_visit
end

Even if the logic is simple and doesn't execute anything if the parameter is not present or it's empty, we can move the set_referral_cookie into a specific controller:

class PagesController < ApplicationController
  set_referral_cookie
end

The cookie method on the Refer class is what gets stored in the cookie, and it returns something like this:

{:value=>"7byZBXjN", :expires=>2025-08-14 01:11:06.580137000 UTC +00:00}

The expiration date is set to 30 days after the method is executed, but we can customize it with the cookie_length method:

# config/initializers/refer.rb
Refer.cookie_length = 15.days

If there's a code in the cookies, the method performs a query to try to find one and, if it does, it tracks a visit to the code.

We can also customize the code generation by modifying the code_generator attribute with a lambda that receives our generator:

# config/initializers/refer.rb
Refer.code_generator = -> (referrer) { "#{referrer.id}-#{SecureRandom.hex(6)}".upcase } #=> "13-507B9DF87033"

By default, the generator uses SecureRandom.alphanumeric(8) which is fine, but your application might need something different.

User referral

The actual user referral is performed by the Refer.refer class method which receives a code and a user instance that represents the referee:

referee = User.last
code = "123456"
Refer.refer(code, referee)  # Creates a Refer::Referral

This method finds the ReferralCode associated with the code and creates a new Refer:Referral which keeps track of the referrer and referee association like we explained above.

There's an alternative Refer.refer! method that takes the same arguments but returns an AlreadyReferred error if the referee user has already been referred by somebody else. This is especially useful if we're not creating the referral at the time of the user sign-up.

Now that we understand how the gem works, let's add a referral feature to our application:

Adding referrals

Let's start by running the gem's installation command which adds the previously mentioned set_referral_cookie method to the ApplicationController:

bin/rails generate refer:install

Then, we run the command to add referrals to our User model:

bin/rails generate refer:model User

Next, we run the migrations to create all the tables:

bin/rails db:migrate

Now, let's add a callback to create a ReferralCode on user creation:

class User < ApplicationRecord
  after_create :create_referral_code

  private
  def create_referral_code
    referral_codes.create
  end
end

To keep our logic simple, we can also add a referral_code method to the user, which is responsible for returning the latest code:

def referral_code
  referral_code.last.try(:code)
end

If we're starting from an application that already has users, which is pretty probable, we can create a migration to add codes for existing users:

bin/rails generate migration create_referral_codes_for_existing_users

And add the following to the migration:

class CreateReferralCodesForExistingUsers < ActiveRecord::Migration[8.0]
  def up
    User.find_each do |user|
      user.referral_codes.create unless user.referral_codes.any?
    end
  end

  def down
    User.find_each do |user|
      user.referral_codes.destroy_all
    end
  end
end

Now, after running the migration, every user in our application should have an associated referral code that we can use.

Refer a friend view

This view is intended to educate users about our referral program and the steps involved with it.

Let's start by adding a route and a controller for this view:

get "refer-a-friend", to: "pages#refer_a_friend", as: :refer_a_friend
class PagesController < ApplicationController
  def refer_a_friend
  end
end

Then, we add the view code to show the steps for the referral process, a section where users can copy the referral link, and a section for users to send invitations by email.

For brevity purposes, we won't paste the whole view code, but you can check it in the AvoCasts repository. The result for the view looks like this:

Refer a friend view

The main goal for this view is to give our users a concise way to understand the referral process and make it easy for them to share their referral codes with their friends.

To avoid hardcoding the URL, let's add a helper method that receives a User instance and produces the referral code:

module UsersHelper
  def referral_link(user)
    root_url(code: user.referral_code)
  end
end

Next, to make the Send by email feature work, let's start by adding a route:

# config/routes.rb
resources :referral_emails, only: [:create]

Then, let's add the corresponding controller:

class ReferralEmailsController < ApplicationController
  def create
    emails = params[:emails].split(",")
    emails.each do |email|
      FriendReferralMailer.referral_email(current_user, email).deliver_later
    end
  end
end

Now, let's add the mailer:

class FriendReferralMailer < ApplicationMailer
  def referral_email(referrer, referee_email)
    @referrer = referrer
    @referee_email = referee_email
    mail(to: @referee_email, subject: "Hey, #{@user.name} wants you to know about AvoCasts")
  end
end

Consider that allowing users to send emails through our application can be a source for spam even if we're not allowing users to actually change the email content. Consider adding some kind of filter for the email list and use anti-spam techniques like the Cloudflare Turnstile. to avoid unnecessary attacks.

The user dashboard

Stakeholder dashboard

Summary

Adding a referral system is a good way to lower customer acquisition costs while also providing value to referrers and new users.

Within the Rails world, keeping track of user referrals and who referred whom can be achieved using cookies and a couple of models.

However, solutions like the Refer gem make our lives easier by providing most of the work for us.


Build your next rails app 10x faster with Avo

Avo dashboard showcasing data visualizations through area charts, scatterplot, bar chart, pie charts, custom cards, and others.

Find out how Avo can help you build admin experiences with Rails faster, easier and better.