Stripe Connect onboarding with Ruby on Rails
Tags: stripe, rails, tutorial, webdev
Published: 2023-04-14 17:03:12

Stripe Connect provides a suite of tools and APIs that allow you to create, manage, and scale your platform or marketplace and facilitate payments and payouts for your users. If you’re new to Stripe Connect, take a look at the first few articles in this series. In this article you’ll learn how to integrate Stripe Connect Onboarding with Ruby on Rails, so that you can start facilitating money movement for your users.

We'll focus on an email newsletter platform use case, where readers support their favorite authors by paying a monthly fee to receive periodic emails. You’ll learn how to create Stripe Accounts and collect business details using Stripe Connect hosted onboarding. We’ll also cover setting up the Rails environment and some best practices for smooth integration.

Let's get started!


# Creating a new Rails application

We’ll kick things off by breaking ground on a new Rails application that uses Tailwind CSS for styles and Postgres for the database. `-T` means skip adding the default testing infrastructure and `--main` sets the git branch name.


```bash
rails new newsletter-platform -c tailwind -j esbuild -d postgresql -T --main
```


We’ll store each newsletter issue in the database so that authors can direct users to their back catalog if they’d like to read past issues. Before sending a newsletter issue to a reader, we’ll check to make sure readers have an active payment subscription. 

Let’s start by setting up these database models.


## Database setup

To set up the necessary database models for this newsletter platform, we'll create the main models: User, Newsletter, NewsletterIssue, and Subscription. The User model represents the author, the Newsletter model corresponds to each collection of issues, the NewsletterIssue model represents each monthly edition of the newsletter, and the Subscription model links the readers to the newsletters they have active payment subscriptions for. Let's begin by generating these models.

Generating the User model:

The User model is used for authentication. Both readers and authors are represented in the database as Users.

We store the `stripe_customer_id` for readers. We’ll use the Stripe API to create customer objects for all users so that we can keep track of all subscriptions and invoices that are related to a given reader.

We store the `stripe_account_id` for authors. This represents the ID of the author’s Stripe Account and enables us to route payments from readers to the author. We also store `charges_enabled` and `payouts_enabled` flags to know when the account is fully onboarded and can successfully receive money.


```bash
rails generate model User name:string email:string stripe_customer_id:string stripe_account_id:string charges_enabled:boolean payouts_enabled:boolean
```


Generating the Newsletter model:

Newsletters have a foreign key relationship back to the ID of the author in the Users table.


```bash
rails generate model Newsletter user:references title:string
```


Generating the Newsletter Issue model:

Newsletter Issues are related to the newsletter they are about and to start, we’ll simple with a title and block of text for content. The `published_at` datetime enables us to schedule issues to be published in the future. 


```bash
rails generate model NewsletterIssue newsletter:references subject:string content:text published_at:datetime
```


Generating the Subscription model:

When a reader subscribes to a newsletter for the first time, we’ll send them through a payment flow using Stripe Checkout to collect their payment details and start a Stripe Subscription that collects recurring payments. We’ll store that `stripe_subscription_id` in the database so that we can check the Stripe API to know whether payment is active. 


```bash
rails generate model Subscription user:references newsletter:references stripe_subscription_id:string status:string
```


Run the database migrations:


```bash
rails db:migrate
```


Now, let's set up the relationships between these models:

In `app/models/user.rb`:


```ruby
class User < ApplicationRecord
  has_many :newsletters
  has_many :subscriptions
end
```


In `app/models/newsletter.rb`:


```ruby
class Newsletter < ApplicationRecord
  belongs_to :user
  has_many :newsletter_issues
end
```


In `app/models/newsletter_issue.rb`:


```ruby
class NewsletterIssue < ApplicationRecord
  belongs_to :newsletter
end
```


In `app/models/subscription.rb`:


```ruby
class Subscription < ApplicationRecord
  belongs_to :user
  belongs_to :newsletter
end
```


With these models in place, we can now represent authors, newsletters and their issues, and reader subscriptions in our database. In the next steps, we'll implement the Stripe Connect Onboarding logic.


## Set up Stripe

Use the [stripe-ruby](https://github.com/stripe/stripe-ruby) SDK to interact with the Stripe API. 


```
bundle add stripe
```


Retrieve API keys from dashboard.stripe.com and set those into our Rails credentials.


```
EDITOR=vi rails credentials:edit
```


Insert the keys like so:


```yaml
stripe:
  secret_key: sk_test_51EceeUCZ6qs...
  publishable_key: pk_test_vAZ3gh1Lc...
```


Add an initializer to `config/initializers/stripe.rb` in order to set the platform level API key. Note that each author’s Stripe account will have its own API keys, but with Stripe Connect, we never need the connected account’s keys. Instead we authenticate requests for connected accounts with the combination of our platform level API key and the connected accounts’ ID. See [here](https://stripe.com/docs/api/connected_accounts) for more details.


```ruby
Stripe.api_key = Rails.application.credentials.dig(:stripe, :secret_key)
```


Now we’re ready to start making API calls to Stripe from Ruby. Before we start using the Stripe API, let’s get authentication up and running.


## Set up authentication

For this use-case, users need a way to authenticate into the application. Since we’re using Ruby on Rails, we’ll use [devise](https://github.com/heartcombo/devise) authentication for authors.

We’ll install the devise gem, run the install scripts and generate the routes and migration for Authors to be database authenticatable. 


```bash
bundle add devise
rails generate devise:install
rails g devise:views
rails g devise Author
```


Again, we migrate the database.


```bash
rails db:migrate
```


Running `bin/dev` fires up the server so we can test our registration flow at localhost:3000/authors/sign_up. 

We’re presented this unstyled login view that we’ll clean up later. 

Now that users can register for the application, we’ll want to onboard them to Stripe Connect so that we can start interacting with the Stripe API on their behalf.


## Set up Connect onboarding

Connect supports 3 different account types: Standard, Express, and Custom. Since authors might not have experience handling refunds, and chargebacks in this use case, it makes sense to provide them an integration where they have access to the simpler Stripe dashboard with a **Express-type** connected account. Learn more about the tradeoffs between different account types [here](https://stripe.com/docs/connect/accounts). Side note: we’re hoping to remove the concept of an account type so stay tuned for a less confusing approach to differentiating Connect functionality for your users.

For onboarding, we’ll have a page where we’ll either show the account details for the connected account, or a button for the user to create a new account and go through the onboarding process. These functions will be handled by a new StripeAccountsController.


```bash
rails g controller StripeAccounts show
```


We’ll add singular resource routes for /stripe_account by updating config/routes.rb. 


```ruby
Rails.application.routes.draw do
  resource :stripe_account
  devise_for :users
end
```


Next, we’ll add the before action macro from devise that requires an authenticated author to access these routes.


```ruby
class StripeAccountsController < ApplicationController
  before_action :authenticate_author!
end
```


The view for the show route is very simple for now.


```html
<% if current_user.stripe_account_id.present? %>
  <%= current_user.stripe_account.to_json %>
<% else %>
  <p>No Stripe account found</p>
  <%= button_to "Create a Stripe Account", stripe_account_path, method: :post, data: { turbo: false } %>
<% end %>
```


When a user clicks on “Create a Stripe Account”, we’ll first make an API call to Stripe to create a new Express account, then we’ll update the database with the account’s ID and finally redirect through the account onboarding flow with an Account Link.

The goal here is to minimize the amount of information the author needs to re-enter. For instance, we already have the author’s email address, so we can prefill that at the account and individual level. We’ll also assume that all authors are individuals rather than businesses. We can prefill the business profile’s [mcc (merchant category code)](https://stripe.com/docs/connect/setting-mcc#list) as digital goods so that each author isn’t required to dig through the list of service types to find digital goods.


```ruby
  def create
    account = Stripe::Account.create(
      type: 'standard',
      email: current_user.email,
      business_type: 'individual',
      business_profile: {
        mcc: '5818',
      },      
      individual: {
        email: current_user.email,
      },
      metadata: {
        author_id: current_user.id,
      }
    )
    current_user.update(stripe_account_id: account.id)

    account_link = Stripe::AccountLink.create(
      account: account.id,
      refresh_url: stripe_account_url,
      return_url: stripe_account_url,
      type: 'account_onboarding'
    )

    redirect_to account_link.url, status: :see_other, allow_other_host: true
  end
```


When events related to Stripe accounts happen in Stripe, the application can be notified using webhooks. We need to listen for the `account.updated` webhook event type so that we know when an account has successfully completed onboarding. 


## Set up webhooks

We need a controller to handle the incoming POST requests from Stripe. 


```bash
rails g controller Webhooks
```


We’ll add a simple route of /webhooks for handling POST requests.


```ruby
resources :webhooks, only: [:create]
```


Since the requests come from Stripe, we cannot verify any CSRF token, so we skip that check at the top of the controller.


```ruby
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token
```


Then we’ll add a create method for handling post requests from Stripe to deserialize the event body and switch based on the type of event notification.


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

    begin
      event = Stripe::Event.construct_from(
        JSON.parse(payload, symbolize_names: true)
      )
    rescue JSON::ParserError => e
      # Invalid payload
      puts "⚠️  Webhook error while parsing basic request. #{e.message})"
      render json: { message: 'failed' }, status: 400
      return
    end

    case event.type
    when 'account.updated'
      account = event.data.object # contains a Stripe::Account
      # TODO: Handle account updates
    else
      puts "Unhandled event type: #{event.type}"
    end

    render json: { message: 'success' }
  end
```


Each time we receive the account.updated event we want to update our local Author’s flags for whether charges and payouts are enabled.


```ruby
    when 'account.updated'
      account = event.data.object # contains a Stripe::Account
      author = User.find_by(stripe_account_id: account.id)
      author.update(
        charges_enabled: account.charges_enabled,
        payouts_enabled: account.payouts_enabled
      )
```


To build and test webhooks locally, we’ll use the Stripe CLI. The `listen` command enables us to forward both account and connect webhook events. Account a.k.a. direct events are those events that fire on our account, connect events are those that happen on connected accounts. 

Start the listener with:


```bash
stripe listen --forward-to localhost:3000/webhooks --forward-connect-to localhost:3000/webhooks
```


As a shortcut with Rails, I add a new process to Procfile.dev so that this starts each time we run bin/dev. My Procfile.dev looks like this:


```bash
web: bin/rails server -p 3000
js: yarn build --watch
css: yarn build:css --watch
stripe: stripe listen --forward-to localhost:3000/webhooks --forward-connect-to localhost:3000/webhooks
```


Now we can go through the onboarding flow and enter test details. Use the magic test strings from [this doc](https://stripe.com/docs/connect/testing) to ensure a verified test account. Note you’ll also need to verify your email in order to activate the account, so use a real email address that you have access to when creating the new connect account.

If all went to plan, you see the JSON for the Stripe Account in the browser, your Author has a Stripe Account ID, charges enabled is true, and payouts enabled is true in the database.