DEV Community

Cover image for User registration for SaaS businesses, with a bonus
CJ Avilla for Stripe

Posted on • Updated on

User registration for SaaS businesses, with a bonus

Registration, pricing page, payment, and provisioning are four building blocks of subscription onboarding. In order to provision access, we need (1) a registered user, who has (2) selected their price from a pricing page, and (3) set up their payment information. In this article, we’ll show examples with Ruby on Rails, but the principles apply in any full stack web framework.

Registration

SaaS applications need some way to authenticate (ensure users are who they say they are) and authorize access to (ensure users should be allowed to access the services they are allowed to access). For our example, authorization will be determined by whether or not a user has an active subscription.

Registration is the process of a person becoming a new user of a SaaS application, enabling them to authenticate when they come back later. You’ve seen this flow a million times: click “join” or “register” or “sign up” > enter an email or username and a password, probably a password confirmation > maybe go check your email to verify it’s you.

At the end of the registration flow, a new User record is stored somewhere in a database. In some SaaS applications you’ll see the concept of a Team or Organization where several users will share access to the same services. We’ll eventually need to decide if we want to bill at the User level or at the Team level. This article won’t cover the details of charging per-seat, but just know that per-seat subscriptions are very similar to a fixed-price subscription, except they pass a quantity for a line item representing the number of seats.

User vs Team with users

A Stripe Customer represents a customer of your business. It lets you create recurring charges and track payments that belong to the same customer. If we plan to bill at the User level, we’ll associate a Stripe Customer to the User, if we plan to bill at the Team level, we’ll associate a Stripe Customer to the Team.

User or team relation to the Stripe Customer

Some Stripe payment flows will create a Stripe Customer automatically for us. Using Payment Links or the embeddable pricing table will both result in a new Stripe Customer. If we don’t specify a Customer ID with Stripe Checkout in subscription mode, it’ll also create a Stripe Customer when the user subscribes.

Customer creation

We have a choice to make: should we create the Stripe Customer object ourselves or let Stripe handle that as part of an existing payment flow?

On one hand, if we create the Stripe Customer ahead of time we can ensure that all future actions are associated with a specific Customer. On the other hand, it is convenient to use Payment Links (no-code) or the embeddable pricing table (very low-code drop-in component). If we use Payment Links or Pricing table, we can include reference to the User object with the client_reference_id. Stripe Checkout also supports passing a client_reference_id.

Note: For the most flexibility and to interoperate with the most Stripe products, let Stripe create the Customer object for you and use the client_reference_id to keep track of which User is subscribing.

If you want more control, don’t want to use the embeddable pricing table or Payment Links, and want some more advanced testing features, use the API create a new Stripe Customer when a new User registers. This is the only route if you want to use Test Clocks — covered in the bonus section below!

Frequently asked questions about SaaS onboarding

If I’m using the embeddable pricing table, how do I know which user is subscribing?

The pricing table web component accepts a property for client-reference-id where you can pass the ID of the User, or or Team object:

<script async src="https://js.stripe.com/v3/pricing-table.js"></script>
<stripe-pricing-table
  pricing-table-id="prctbl_1LLsmTCZ6qsJgndJRFzV9Jia"
  publishable-key="pk_test_vAZ3gh1LcuM7fW4rKNvqafgB00DR9RKOjN"
  client-reference-id="<%= current_user.id %>">
</stripe-pricing-table>
Enter fullscreen mode Exit fullscreen mode

The webhook notifications about the resulting Checkout Session will contain the global ID in the client_reference_id property in the payload, so you can resolve which user a given Checkout Session is associated with.

Should I create the Stripe Customer before or after the User is committed to the database if I’m using the API instead of letting Stripe create the Customer for me?

We recommend creating the Stripe Customer after the user is fully committed to the database because there isn't an easy way to roll back the API call to create the Customer. There are several reasons why a User record might not successfully be written to the database: e.g., uniqueness constraint fails where a user with the same email address tried to register twice.

What information should I pass to Stripe when I create the Customer object?

You don't need to pass any information. However, I recommend creating complete customers with as much context as possible. This will make your life easier later when you need to handle support requests for refunds, disputes, and other administrative tasks. I also recommend storing the ID of the user from your database in the metadata of the Stripe Customer.

params = {
  name: user.name,
  email: user.email,
  phone: user.phone,
  address: {
    line1: user.address_line1,
    city: user.address_city,
    state: user.address_state,
    postal_code: user.address_postal_code,
    country: user.address_country,
  },
  shipping: {
    name: user.name,
    phone: user.phone,
    address: {
      line1: user.address_line1,
      city: user.address_city,
      state: user.address_state,
      postal_code: user.address_postal_code,
      country: user.address_country,
    },
  },
  metadata: {
    user_id: user.id,
  },
}
customer = Stripe::Customer.create(params)
Enter fullscreen mode Exit fullscreen mode

What information about the Stripe Customer, if any, should I store in my database? Does each User need a Customer object? Each Team? Is there a perfect 1:1 relationship between a User in my database and a Customer on Stripe?

Again, you don't technically need to store anything in your database. However, I recommend storing at least the string ID of the Stripe Customer (which looks like cus_abc123) in your database. You can store this directly on the users or teams table, or create a separate table for storing Stripe Customer IDs and their relation to PaymentMethods and Subscriptions. Maintaining a separate table offers a bit more flexibility later if you decide to change from supporting single users to teams. It also separates concerns. Have a look at the open source pay-rails database schema or the cashier-stripe migrations for inspiration for modeling your database. If you’d like more suggestions about how to model your database to work well with SaaS, let me know on Twitter: @cjav_dev.

Since an API call takes some time, should I send the API request to create the Stripe Customer in the background with a job?

Conversions of guests to paying users is a time-sensitive process so one best practice is to keep that experience as snappy as possible. If you already have a system in place for processing jobs asynchronously, then I recommend creating the customer and updating the user in a background job that is scheduled right after the user is committed to the database.

If you don't already have background jobs in your application, it's fast enough to create the Stripe Customer synchronously and come back to this later as you scale.

class CreateStripeCustomerJob < ApplicationJob
  queue_as :default
  def perform(user)
    params = {
      email: user.email,
      # ...
      metadata: {
        user_id: user.id,
      }
    }
    customer = Stripe::Customer.create(params)
    user.update!(stripe_customer_id: customer.id)
  end
end

class User < ApplicationRecord
  after_commit :ensure_stripe_customer
  def ensure_stripe_customer
    return if stripe_customer_id.present?

    CreateStripeCustomerJob.perform_later(self)
  end
end
Enter fullscreen mode Exit fullscreen mode

What if I already have many users from a successful beta program that launched before payments?

There are a couple options for backfilling existing users and creating Stripe Customer objects for them. You could run a one-time migration, or update your onboarding flow so that it lazily creates Stripe Customers if none exists each time an existing user comes back. I prefer running a migration and think that's the easier of the two options.

(Bonus!) Test clocks

As a super duper bonus, I wanted to share a pattern that I've been using lately in development to make it easier to build and test future payments.

Test clocks are a relatively new testmode feature that enable you to simulate traveling through time to check that your application properly handles all of the webhook events that may eventually happen (think payment failures or upgrades to higher plan levels).

Test clocks must be associated with a Customer when the Customer object is created. This means we can only use test clocks when we explicitly create the Customer. Note that test clocks are not compatible with the pricing table or Payment Links because there isn’t a way to use existing customers with these integrations, yet. You can still test your webhook logic, but you’ll need to manually create a customer and subscription using the API, then advance the clock to test different webhook scenarios.

If we're in development, create a test clock and attach it to the newly created customer.

params = {
  email: user.email,
  # ...
  metadata: {
    user_id: user.id,
  }
}
if Rails.env.development?
  test_clock = Stripe::TestHelpers::TestClock.create(
    frozen_time: Time.now.to_i,
  )
  params[:test_clock] = test_clock.id
end
customer = Stripe::Customer.create(params)
Enter fullscreen mode Exit fullscreen mode

Later on, when we create a subscription for this customer, we can go into the Stripe Dashboard and advance the clock to a future date, which will result in several webhook events being sent to your webhook handler (something we'll discuss later on in this series during provisioning).

Checkout this episode about test clocks to learn more. Stay tuned for the next article to learn more about building pricing pages.

About the author

CJ Avilla

CJ Avilla (@cjav_dev) is a Developer Advocate at Stripe, a Ruby on Rails developer, and a YouTuber. He loves learning and teaching new programming languages and web frameworks. When he’s not at his computer, he’s spending time with his family or on a bike ride 🚲.

Top comments (0)