Stripe Connect onboarding with Ruby on Rails
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.
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.
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.
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.
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.
rails generate model Subscription user:references newsletter:references stripe_subscription_id:string status:string
Run the database migrations:
rails db:migrate
Now, let's set up the relationships between these models:
In app/models/user.rb
:
class User < ApplicationRecord
has_many :newsletters
has_many :subscriptions
end
In app/models/newsletter.rb
:
class Newsletter < ApplicationRecord
belongs_to :user
has_many :newsletter_issues
end
In app/models/newsletter_issue.rb
:
class NewsletterIssue < ApplicationRecord
belongs_to :newsletter
end
In app/models/subscription.rb
:
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 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:
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 for more details.
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 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.
bundle add devise
rails generate devise:install
rails g devise:views
rails g devise Author
Again, we migrate the database.
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. 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.
rails g controller StripeAccounts show
We’ll add singular resource routes for /stripe_account by updating config/routes.rb.
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.
class StripeAccountsController < ApplicationController
before_action :authenticate_author!
end
The view for the show route is very simple for now.
<% 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) as digital goods so that each author isn’t required to dig through the list of service types to find digital goods.
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.
rails g controller Webhooks
We’ll add a simple route of /webhooks for handling POST requests.
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.
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.
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.
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:
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:
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 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.