Easy SaaS subscription flow with Stripe Checkout in Rails
Published: 2022-09-25 11:56:07

---
title: Easy SaaS subscription flow with Stripe Checkout in Rails
published: true
date: 2022-09-18 07:00:42 UTC
tags: 
canonical_url: https://rubyist.info/saas-subscription-flow-with-stripe-checkout-in-rails
---

What we are going to build is a Stripe checkout integration for subscription flow of a SaaS app like this:

![stripe-saas-susbcription-flow.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1663453619075/2MbkFmeeP.gif)

So the idea of the sequence is:

User views landing page

user clicks on one of the plans on pricing table

user gets redirected to the Stripe Checkout page

User fills the form and completes the checkout

user gets redirected to the registration form on our website

user adds password for authentication

user registered and authenticated, with subscription already provisioned.

In this guide, were assuming well sell a subscription of our web app in three tiers and only serve customers with USD and EUR as the currencies.

All the code in this article is available on [this github repo](https://github.com/galliani/stripe-subscription-sample).

## Modelling user and account

On this guide, the assumption is that we are using devise for our authentication,

```ruby
rails g devise:install

```

that means by running the above command, well already have users table and User model.

To model the Stripe Customer object

```ruby
rails g model Account email stripe_customer_id
rails g model AccountUser user:references account:references role


class AccountUser < ApplicationRecord
    belongs_to :account
    belongs_to :user
end


class User < ApplicationRecord
    has_many :account_users
    has_many :accounts, through: :account_users
    # "devise" line added automatically by devise, no big deal. But notice we remove :confirmable
    devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :lockable, :trackable
end


class Account < ApplicationRecord
    has_many :account_users
    has_many :users, through: :account_users, source: :user
end

```

## Modeling the product and plan

For you not familiar with Stripe, Product object refers to something that you charge your customer for, basically things you sell be it a tangible goods like clothes or intangible things like subscription. Plan (or Price in Stripe) on the other hand, is merely the pricing unit for the Product, usually depicted in form of currency (USD, EUR, etc) and interval (monthly, quarterly, yearly).

For the Product, since there wont be many changes we expect to make for this kind of records, lets just model it into static values using a ruby class. You can make this in form of Ruby PORO, but to make it easier and cleaner, lets use `ActiveHash` gem for this.

`Gemfile`

```
gem 'active_hash'

```

Add the above line to your `Gemfile` then run `bundle install`.

After that create a file called `product.rb` inside the `app/models/` directory.

```ruby
class Product < ActiveHash::Base
  fields :stripe_product_id, :name, :tier, :unit_amounts
  create id: 1, stripe_product_id: 'LITE', name: 'Lite', tier: 'lite', unit_amounts: {
        month: { eur: 1000, usd: 1200 },
        year: { eur: 10000, usd: 12000 }
    }
  create id: 2, stripe_product_id: 'PRO', name: 'Pro', tier: 'pro', unit_amounts: {
        month: { eur: 2000, usd: 2400 },
        year: { eur: 20000, usd: 24000 }
    }
  create id: 3, stripe_product_id: 'PREMIUM', name: 'Premium', tier: 'premium', unit_amounts: {
        month: { eur: 4000, usd: 4800 },
        year: { eur: 40000, usd: 48000 }
    }
end

```

Now for the pricing object which is the `Plan`, we can do similar thing like the `Product`, but for this we can predict there will be more dynamism to the data, hence we go for full-blown database table. So lets run Rails scaffold for generating the model.

```
rails g model Plan product_id interval currency nickname unit_amount stripe_price_id

```

The scaffold command will automaticall generate the `app/models/plan.rb`, make sure after that you make that class similar to this below:

```ruby
class Plan < ApplicationRecord
  # to link the db-backed Plan model with static ActiveHash model of Product
  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to_active_hash :product

  delegate :tier, to: :product, prefix: true
end

```

## Modeling the subscription and link it to the user

By running the command below in terminal, you will have subscription table and model automatically generated.

```
rails g model Subscription account:references plan:references stripe_subscription_id status

```

Go to the subscription model `app/models/subscription.rb` and make sure the model looks like this.

```ruby
class Subscription < ApplicationRecord
  belongs_to :account
  belongs_to :plan

  delegate :product_tier, to: :plan, prefix: false
end

```

Before we forget, lets check the account and user models and add the lines below to make it easier to interact with the newly-created subscription model.

```ruby
class Account < ApplicationRecord
    ## add this
    has_many :subscriptions
    has_one :current_subscription, -> { order(id: :desc) }, class_name: "Subscription"
end

class User < ApplicationRecord
    ## add this
    has_many :subscriptions, through: :accounts, source: :subscription
end

```

after all this, dont forget to run `rails db:migrate` .

## Install ruby Stripe and Dotenv gems

We need to install Stripe ruby SDK gem, so shortly we can have with the Stripe API!

Add this to the `Gemfile`

```
gem 'stripe'
gem 'dotenv-rails', groups: [:development, :test]

```

Then run `bundle install`.

To store sensitive values such as API key for interacting with Stripe API, we can use `.env` file. Create `.env` file on the root of your repo and add your Stripe API key that you can retrieve from the developer dashboard.

```
STRIPE_SECRET_KEY=sk_test_111111111

```

Now that we have added the API key on the environment variable file, then create initializer file for the Stripe gem `config/initializers/stripe.rb` to set that key automatically.

```ruby
Stripe.api_key = ENV['STRIPE_SECRET_KEY']

```

## Seed Stripe product & price

To make it easier for setting and synchronizing up the data both in our database and Stripes one, we should have a script for this. You can make it as a rake task, but for the simplicity of running it, well use Rails seeds file.

`db/seeds.rb`

```ruby
Product.all.each do |product|
  begin
    Stripe::Product.create(
      id: product.stripe_product_id,
      name: product.name
    )
  rescue Stripe::StripeError => error
  end

  # Fetch existing stripe prices of this product
  existing_stripe_prices = Stripe::Price.list(product: product.stripe_product_id)
  existing_stripe_prices.data.select do |price|
    plan = Plan.where(
      interval: price.recurring.interval,
      currency: price.currency.to_s,
      unit_amount: price.unit_amount,
      product_id: product.id
    ).first_or_initialize

    # this will enable us to sync the db records with Stripe
    plan.stripe_price_id = price.id
    plan.save
  end

  product.unit_amounts.each do |interval, data|
    data.each do |currency, amount|
      plan = Plan.where(
        interval: interval, currency: currency.to_s, unit_amount: amount, product_id: product.id
      ).first_or_initialize

      # skip creating the price in Stripe if already synced
      next if plan.stripe_price_id.present?

      stripe_price = Stripe::Price.create(
        product: plan.product.stripe_product_id,
        currency: plan.currency,
        unit_amount: plan.unit_amount,
        nickname: plan.nickname,
        recurring: { interval: plan.interval }
      )
      plan.update(stripe_price_id: stripe_price.id)
    end
  end
end

```

Whenever we want to seed or sync data to our database, we would just need to run this command:

`rails db:seed`

Do this if you have not, by the way, to continue follow along the tutorial. By now, we expect you already have records in both the Product and the Plan tables in the database, and also the matching records in Stripe.

## Embed Stripe Pricing tables and Checkout

Create pricing table on Stripe Dashboard.

![Untitled.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1663451733740/2szilKatY.png)

![Untitled (1).png](https://cdn.hashnode.com/res/hashnode/image/upload/v1663451751257/1U_ARG4cO.png)

Add the pricing table on your landing page or pricing page, but in our case well add it to the landing page directly

```
<script async src="https://js.stripe.com/v3/pricing-table.js"></script>
<stripe-pricing-table pricing-table-id="prctbl_1LZ8xoxoxoxox"
publishable-key="pk_test_90Jbzzzzzzzzzzzzzz">
</stripe-pricing-table>

```

Obviously we should not commit those static values of the `pricing-table-id` and `publishable-key`, so let's utilize the `.env` variable we created earlier.

```
STRIPE_PUBLISHABLE_KEY=pk_test_90Jbzzzzzzzzzzzzzz
STRIPE_PRICING_TABLE_ID=prctbl_1LZ8xoxoxoxox


<body>
  <h1>We offer plans that help any business!</h1>
  <!-- Paste your embed code script here. -->
  <script
    async
    src="https://js.stripe.com/v3/pricing-table.js">
  </script>
  <stripe-pricing-table
    pricing-table-id="<%= ENV['STRIPE_PRICING_TABLE_ID'] %>"
    publishable-key="<%= ENV['STRIPE_PUBLISHABLE_KEY'] %>"
  >
  </stripe-pricing-table>
</body>

```

## Add webhooks for listening to the checkout

Before we start writing the code for the webhook, make sure **we have installed the Stripe CLI so that we can receive the webhook locally**. You can do that easily by following the steps written on this [URL](https://stripe.com/docs/webhooks/quickstart#download). Once you have finished the setup, let's continue.

To be ready for the webhook, lets add a new endpoint to the `routes.rb`.

`config/routes.rb`

```ruby
    resources :stripe_events, only: :create

```

Then of course create new controller for working as the webhook.

```ruby
class StripeEventsController < ActionController::Base
    # The skip below here is very important
    skip_before_filter :verify_authenticity_token

  def create
        payload = request.body.read
      event = nil

      begin
        event = Stripe::Event.construct_from(
          JSON.parse(payload, symbolize_names: true)
        )
            stripe_id = event.data[:object][:id]
      rescue JSON::ParserError => e
        # Invalid payload
        render status: 400
        return
      end

      # Handle the event
      case event.type
        when 'checkout.session.completed'
      else
        raise "Unhandled event type: #{event.type}"
      end

      render status: 200
  end
end

```

For this skeleton endpoint, there are a number of important things:

- The `StripeEventsController` inherit from `ActionController::Base` instead of `ApplicationController`, this is to ensure that the controller will not share any existing callbacks listed on `ApplicationController`. Otherwise youd have to skip those unrelated, redundant callbacks manually on this controller.
- The second is the `skip_before_filter :verify_authenticity_token` line which is important, because otherwise youd get `422 Unprocessable Entity` as response when the webhook endpoint gets hit by Stripe.

```ruby
when 'checkout.session.completed'
  stripe_checkout = event.data.object
  stripe_customer_id = stripe_checkout.customer
  stripe_subscription_id = stripe_checkout.subscription

  account = Account.where(
    stripe_customer_id: stripe_customer_id,
    email: stripe_checkout.customer_details.email
  ).first_or_create

  stripe_subscription = Stripe::Subscription.retrieve(stripe_subscription_id)

  # make a loop of this if you expect that the subscription can contain many plans
  stripe_price = stripe_subscription.items.data[0].price
  plan = Plan.find_by(stripe_price_id: stripe_price.id)

  subscription = Subscription.where(stripe_subscription_id: stripe_subscription_id).first_or_initialize
  subscription.assign_attributes(
    plan_id: plan.id,
    account_id: account.id,
    status: stripe_subscription.status
  )
  subscription.save

```

Since were using Stripe Checkout, we can actually achieve what weve done above with only one event, which is the `checkout.session.completed` event, instead of two. The reason youd want to use the `checkout.session.completed` event instead would be the simplicity of having to listen to and handle only one event instead of more.

## Handle redirect after checkout

So the idea is to redirect the customer to the registration form so they can set the password to their new account, once the payment is complete. Since were using Devise on this tutorial, then well use Devises existing form page and controller to do that. If youre using different authentication engine, then you will need to adjust accordingly.

That is to generate the form page, if you havent already done so. For the controller, this is the command:

```
rails generate devise:controllers users

```

`config/routes.rb`

```ruby
Rails.application.routes.draw do
    ###
    devise_for :users, controllers: {
    registrations: 'users/registrations'
  }
  get '/checkouts/:id/complete', to: redirect('/users/sign_up?stripe_checkout_id=%{id}')
    ###
end

```

Notice that we specify the `registrations` of devise to be handled by the controller we generated from the `rails generate devise:controllers users`

Go back to the pricing table on the Stripe dashboard and edit it. Once you click continue, you can see the field for the redirect url. Fill it to your local development and add the route [`http://lvh.me:3000](http://lvh.me:3000/)/checkouts/{CHECKOUT\_SESSION\_ID}/complete`. For this tutorial, we are assuming that you will be using`lvh.me` for your local development.

Now that well be redirecting them to the Devise registration controller, we will need to adjust the registration controller just a little bit.

```ruby
class Users::RegistrationsController < Devise::RegistrationsController
    before_action :memoize_checkout, only: :new
    after_action :relate_account, only: :create

    # Add these lines on the bottom of the controller
    private

    # This method is for prefilling the email field for the registration form
    # with the email the customer used to checkout on Stripe Checkout earlier
    def build_resource(hash = {})
    self.resource = resource_class.new_with_session(
      hash.merge(
        email: session['stripe_checkout']['customer_details']['email']
      ),
      session
    )
  end

    # This method is for storing the checkout session object to the session,
    # once the customer gets redirected from Stripe Checkout
    def memoize_checkout
    return unless params[:stripe_checkout_id]
    session[:stripe_checkout] ||= Stripe::Checkout::Session.retrieve params[:stripe_checkout_id].as_json
  end

    # This is for hooking up the newly created user account after registration is successful
    # with the account object, to link user and the paid subscription
    def relate_account
        # alternative 1: find account by email
        # account = Account.find_by_email signup_params[:email]
        # alternative 2: find account by retrieving Stripe customer id from Stripe
        account = Account.find_by(stripe_customer_id: session['stripe_checkout']['customer'])
        # Associate the matching Stripe customer object, our Account object, and the newly-registered User object.
        account.account_users << resource
        account.save
    end
end

```

Before we start testing, we might want to redirect the user to different page after registration is successful, we can do this by:

```ruby
# config/routes.rb
Rails.application.routes.draw do
    authenticate :user do
    get "dashboard" => "home#dashboard", as: :user_root
  end
end

# app/controllers/home_controller.rb
class HomeController < ApplicationController
    # add this endpoint
    def dashboard
  end
end

# app/views/home/dashboard.html.erb
<h2>Dashboard</h2>
<p>Thank you for registering!</p>

```

## One last thing

By this point, you should have a complete end-to-end integration from the checkout and the registration, and the eventual authentication. Your customer would have their payments related correctly to their user record via the account object. So the last step below is entirely optional since it is a nice-to-have.

To make it easier to reference the `current_user` s account and subscription, we will create some helpers. First, we go back to the registration controller.

```ruby
    def relate_account
        account = Account.find_by(stripe_customer_id: session[:stripe_customer_id])
        account.account_users << resource
        # we just change the last line of the method
        if account.save
            session[:current_account_id] = account.id
            session.delete(:stripe_checkout)
        end
    end

```

The `current_account_id` session key will be used to determine the `account` of the currently logged in user. So this will be just as useful as `current_user` to scope resources/records. For example, only

```ruby
class ApplicationController < ActionController::Base

    def current_account
        @current_account ||= current_user.accounts.find(session[:current_account_id])
    end
    helper_method :current_account

    def current_subscription
    @current_subscription ||= current_account.current_subscription
  end
    helper_method :current_subscription
end

```

With this, for example if you want to check if the current\_user is allowed to access certain pages/dashboards based on their current subscription:

```ruby
class ArticlesController < ApplicationController
    before_action :allow_only_premium_users, only: :restricted_page

    def index
    end

    def restricted_page
  end

    private

    def allow_only_premium_users
        redirect_to '/404' if current_subscription.product_tier != 'premium'
    end
end

```

Now, in most apps, there are two places where we authenticate the user, the first is the registration and the other one is the login (session create). So we then need to ensure the `current_account_id` `session` key gets set everytime customer re-login back to the app.

```ruby
class Users::SessionsController < Devise::RegistrationsController
    after_action :set_current_account_id, only: [:create]

    private

    def set_current_account_id
        # find the last used account by relying on the timestamp
        # this way, for example if you implement multi-accounts for a given user,
        # you just need to update the timestamp everytime a user swtiches the account
        session[:current_account_id] = current_user.user_accounts.order(updated_at: :desc).first.id
    end
end

```

If you want to be super clean, you can refactor this a bit better:

```ruby
class UserAccount < ApplicationRecord
    ###
    scope :order_by_recently_accessed, -> { order(updated_at: :desc) }
    ###
end


class User
    has_one :recent_account_user, -> { order_by_recently_accessed}, class_name: 'AccountUser'
end

```

then the previous `after_action` becomes like this:

```ruby
    def set_current_account_id
        # find the last used account by relying on the timestamp
        # this way, for example if you implement multi-accounts for a given user,
        # you just need to update the timestamp everytime a user swtiches the account
        session[:current_account_id] = current_user.recent_account_user.id
    end

```

Thats it, now weve got a super integrated registration flow with checkout easily enabled using Stripe Pricing Table + Stripe Checkout combo! Remember that you can always access the code [here](https://github.com/galliani/stripe-subscription-sample).