DEV Community

Cory Zue
Cory Zue

Posted on • Originally published at saaspegasus.com

Creating a Subscription SaaS Application with Django and Stripe

Django + Stripe = Win

Software-as-a-service (SaaS) subscription businesses are among the fastest-growing companies in the world today. Every day, developers and aspiring entrepreneurs break code on a new subscription SaaS product. But what do these apps look like under the hood?

This guide will cover all the technical details of creating a subscription SaaS business using the Python-based Django web framework and Stripe payment processor.

Here's an interactive demo of what we'll be building.

By the time you've finished the article you should have solid foundational knowledge of everything needed to build a subscription SaaS application—and if you follow along—a fully-functional implementation of subscriptions in your own Django project.

Who should read this

This guide is written primarily for developers who want to add paid subscriptions to their application.

It is specifically focused on the Django web framework and Stripe subscriptions.

If you're a developer using a different technology stack you'll still benefit from the high-level modeling and architectural sections, but may struggle to follow along with some of the code examples, as we assume basic Python and Django knowledge. We also focus heavily on a Stripe integration—though much of the guide holds for Paypal, Braintree or other payment gateways.

What you'll need

In order to follow along you'll need:

Note: We are not going to start from scratch but rather assume you have a functional Django project that you want to add Stripe subscriptions to. To get started with Django it's recommended to follow the official tutorial.

An overview of subscriptions

Before getting into the details of the integration, we'll first take a moment to cover what subscriptions are and why you would want to use them.

If you're already familiar with subscriptions and convinced you want them, feel free to skim this section and/or skip ahead!

What are subscriptions?

Most businesses that sell software operate in one of two ways:

  1. Charge a single amount for a product or access to a service. This is also known as a one-time sale model. Historically, this was the most common way to sell software, and the model is still common in a lot of desktop software, mobile apps, and games.
  2. Charge a recurring amount on a recurring basis, typically each month or year. This is a subscription model—often also referred to as as a software as a service (SaaS) businesses. Spotify, Netflix, Salesforce, and Zoom are all subscription businesses.

There are other software business models—including advertising-based (e.g. Google, Facebook), or marketplace/transaction-fee based (e.g. AirBNB, Stripe)—but in this post we're focusing on the subscription model.

Why would you want to use subscriptions?

As loads of startup advice will tell you, subscription revenue is the holy grail of business models. Because, instead of collecting a one-time sale from your customers, or relying on consistent traffic for advertising, you collect predictable, recurring revenue from your customers.

"Recurring revenue is the only way."
—Jason Cohen, Founder, WP Engine
Designing the Ideal Bootstrapped Business

Having subscription revenue makes it easier to model and forecast your business, since you can quantitatively learn how many new customers you acquire in a month and how many will will cancel (a.k.a. churn). This allows you to very reliably understand how much money you can expect to earn next month, next quarter, and next year.

Subscriptions are also generally a way to increase a customer's life time value (LTV)—the amount of money the they pay you to use your product over time. By charging a smaller amount on a recurring basis you will typically, over the lifetime of a customer, be able to collect substantially more total revenue than from a single-time purchase.

Even software products that have historically had a very successful one-time-sale model like Adobe Photoshop have now switched to a subscription model.

Adobe Pricing

Remember when Adobe Photoshop "only" cost $1000?

How should you structure your subscriptions?

Ok, so you're convinced you want to offer subscriptions to your product. The next question you'll face, is how to set them up. There are a number of choices you'll have to make, including:

  • How many different pricing tiers will the product have and what will the prices be?
  • Will there be a free tier? What about a trial?
  • What will be included in each tier? Common options include limiting access to certain features, as well as setting limits on usage—e.g. only allowing a certain number of users or events on a particular tier.
  • What billing options will you offer? Most apps offer at least a monthly plan and a discounted annual plan.
  • Will you offer a single pricing structure, or charge based on usage? For example, most email sending platforms charge based on the number of mails you send.

Unfortunately there is no one-size-fits-all answer to these questions. Most of the answers will be specific to the application that you are developing, and as the business owner you are hopefully more qualified than anyone else (certainly this article) to make those choices.

For the purposes of this post, we'll go with one of the most common options: a freemium application with multiple pricing tiers and monthly and annual billing options.

Subscription data modeling

Subscription billing systems are complicated.

They involve many moving parts, both on the setup side (what subscriptions you offer) and on the payment side (the details required to collect a payment from a customer and sign them up to a plan).

On the setup side you need to model what tiers exist, how they map to different features in your application, and how much they cost for various time intervals (e.g. monthly, annual, etc.).

And on the payment side you need to model the individual payment details as well as information about the customer and subscription, including the plan they are on, payment status, and renewal details.

It's a lot of stuff!

Thankfully Stripe has thought this problem through for us and created Stripe billing
to model everything we'll need. Therefore, we'll largely be relying on Stripe's billing models and just annotating and referencing them a bit in our Django application. This drastically simplifies the amount of modeling we have to do on our own.

It also means that by and large Stripe will be the primary source of truth for most information, and our Django application will (mostly) just grab a read-only copy of whatever it needs from Stripe.

The Stripe Billing models we'll be using

Stripe's payment and billing system is large and complex, but for the most part we'll be able to focus on four key models—two on the setup side and two on the payment side.

Stripe Models

By using Stripe's existing data models we can keep most of the complexity outside our application. These are the main models we'll be using, from Stripe Billing.

In setup we'll primarily be using Products and Plans.

From Stripe's documentation:

"Two core models make up subscriptions, the heart of Stripe's recurring billing system: Product and Plan. A Product defines the product or service you offer, while a Plan represents how to charge for that Product. The product can be anything you charge for on a recurring basis, such as a digital SaaS product, a base fee for phone service, or a service that comes to your home and washes your car every week."

In the SaaS world (and in the rest of this example) Products are the primary model that map to your pricing tiers / feature sets—e.g. "Pro" and "Enterprise", and then Plans are the options users have for signing up for those products, e.g. "Monthly", "Annual", or "Student Rate".

SaaS applications often refer to the things that Stripe calls "Products" as "Plans". To help mitigate this confusion, this guide uses the word tiers when referring to the Products/Plans that you offer your end-users.

For payment-processing we'll focus on Subscriptions and Customers.

The main model we'll be referencing is the Subscription—which will allow you to charge a Customer on a recurring basis. However, creating Subscriptions and collecting payments will require also working with Customers and so we'll cover those too.

Some other models will be needed to collect card and payment details, but they're not critical to the architecture of our system and we'll discuss them when we encounter them.

Modeling and syncing data between your application and Stripe

So, at a high level we're going to keep our Products, Plans, Subscriptions and Customers in Stripe. But we still need to be able to reference them in our own application so we can do things like:

  • Show a pricing page with our different pricing tiers on it
  • Determine whether a user has an active subscription so they can access a particular feature
  • Send invoices or payment reminders to our customers

So how will we them up?

As we mentioned above, we'll mostly thinking of Stripe as the "master" copy of our data and treat our application as a read-only replica.

What does that look like, practically?

There are two possible approaches.

Approach 1: Store the Stripe IDs of the various objects we'll be using.

In this approach, all we ever store is the Stripe ID of the object in question. Then, whenever we need more information about that object—say to find out the price of a particular Plan—we'd query the Stripe API with the appropriate ID and get back the information we need.

class MyStripeModel(models.Model):
    name = models.CharField(max_length=100)
    stripe_subscription_id = models.CharField(max_length=100)
Enter fullscreen mode Exit fullscreen mode
Approach 1: Referencing just the Stripe IDs.

This keeps things quite simple on our side—we don't need to maintain any local state or worry about keeping data in sync apart from the IDs. Any time we need data, we get it from Stripe and we are guaranteed that the information is up-to-date.

The main problem with this approach is performance. Remote requests are expensive—and so if you're trying to keep your page load times down, minimizing the number of external API calls your application makes can be important. Performance issues can be mitigated with caching, but this isn't always an easy option.

Approach 2: Keep a copy of the Stripe models in your local application database.

In this approach we use code that keeps our Stripe data in sync with our local application database. Then, rather than going back to Stripe every time we need information about a particular piece of data, we can just look it up in our application DB. This solves the performance issue above, and makes it much easier to work with Stripe data—we can just use the ORM we use for the rest of our data.

class StripeSubscription(models.Model):
    start_date = models.DateTimeField(help_text="The start date of the subscription.")
    status = models.CharField(max_length=20, help_text="The status of this subscription.")
    # other data we need about the Subscription from Stripe goes here 


class MyStripeModel(models.Model):
    name = models.CharField(max_length=100)
    stripe_subscription = models.ForeignKey(StripeSubscription)
Enter fullscreen mode Exit fullscreen mode
Approach 2: Keeping a local copy of the Stripe data we need.

The problem with this approach is that, data synchronization is hard.

If we're not careful, our local copy of the data can get out of sync with Stripe, and then bad things can happen to our users. Imagine if someone signed up for a $10/month plan and then got billed $20 for the first month because our data was out of sync! They'd probably be pretty unhappy.

So, which approach is better?

For simple setups it's probably better to go with Approach 1 and only store Stripe IDs. This can get you pretty far and you can always change plans if performance becomes a problem or you encounter workflows that require having more data in your application.

However, specifically for Django applications, we recommend Approach 2. This is primarily because of the great dj-stripe library that handles keeping our data in sync with Stripe with very little effort, allows us to reap the performance benefit of having the data locally, and lets us interface with our Stripe data through the ORM.

If dj-stripe didn't exist, we'd recommend Approach 1 for getting off the ground, but since it does, we'll go with Approach 2 and use it throughout the rest of this guide.

Setting up your Stripe billing models

Ok with our big-picture modeling out of the way we can finally start getting our hands dirty.

The first thing we're going to do is set up our Products and Plans in Stripe. You may want to follow Stripe's guide to setting up a Subscription as we get started. We'll reference this page heavily throughout this article.

It is strongly recommended that you work in your Stripe test account while you are doing development.

This guide will use a relatively generic set of Subscription options: three Products named "Starter", "Standard", and "Premium", with two Plans each ("Monthly" and "Annual").

So, starting with Step 1 in the guide, go ahead and create three Products with the names above (or use your own if you prefer). For each Product, add two pricing plans—one billed monthly and one billed annually. Set your prices up however you like. In our example, we've made the Annual plan cost 10x the monthly (so you get two months free by opting for annual billing—a common SaaS pricing model).

When you're done your Product list should look something like this:

Product List

Your list of Products in Stripe

And in each Product the list of Plans should look something like this:

Standard Plans

The Pricing Plans section for each individual Product

Done? Great!

Let's get coding!

Syncing your Stripe billing data to your Django application

Now that your data is in Stripe it's time to sync it to your Django application.

Remember that library dj-stripe that we mentioned above? This is where it starts to come in handy.

Setting up and configuring dj-stripe

First we'll need to setup dj-stripe. Follow the instructions on their installation documentation, by running pip install dj-stripe (and/or adding it to your requirements.txt file) and adding the "djstripe" app to your INSTALLED_APPS in settings.py like below.

INSTALLED_APPS =(
    # other apps here
    "djstripe",
)
Enter fullscreen mode Exit fullscreen mode

You will also need to set the API keys in your settings.py. As already mentioned, we'll be using test mode, so make sure at least the variables below are set. You can your keys from this page.

STRIPE_TEST_PUBLIC_KEY = os.environ.get("STRIPE_TEST_PUBLIC_KEY", "<your publishable key>")
STRIPE_TEST_SECRET_KEY = os.environ.get("STRIPE_TEST_SECRET_KEY", "<your secret key>")
STRIPE_LIVE_MODE = False
DJSTRIPE_WEBHOOK_SECRET = "whsec_xxx"  # We don't use this, but it must be set
Enter fullscreen mode Exit fullscreen mode

This example allows you to use os environment variables so you don't have to store your secrets in a .py file. However, if you're not familiar with environment variables and are setting things up locally with your test account it's fine to add the keys directly where it says "<your key>".

Note: Make sure never to check any keys into source control!

Once you've added your keys, you will need to create the dj-stripe database tables:

./manage.py migrate
Enter fullscreen mode Exit fullscreen mode

If this command fails it's likely that something isn't set up properly (the command should provide more details). If that happens, double check your setup and make sure it's working before continuing on.

Bootstrapping your initial Products and Plans in Django

With dj-stripe set up, syncing our Products and Plans is now trivial.

Just run the following built-in command:

python manage.py djstripe_sync_plans_from_stripe
Enter fullscreen mode Exit fullscreen mode

If everything is setup properly you should see output that looks like this:

Synchronized plan plan_GzAdqfExNKGmPz
Synchronized plan plan_GzAbnphUgi7vLI
Synchronized plan plan_GqvX7B8467f2Cj
Synchronized plan plan_GqvXkzAvxlF0wR
Synchronized plan plan_GqvV8KsEKyjzSN
Synchronized plan plan_GqvV4aKw0sh0Za
Enter fullscreen mode Exit fullscreen mode

You should see one Plan ID per pricing plan you set up (6 total if you used the suggested setup above).

What just happened?

Behind the scenes dj-stripe looked into your Stripe account, found all your Products and Plans and synced them to your local database. If you go to your local Django Admin UI (by default at http://localhost:8000/admin/djstripe/product/) you should now see the Stripe Products you set up earlier.

Products in Django Admin

Your Plans should look something like this in the Django admin interface

Why was this useful?

Well, now that we have the data in our database we can start using it in our Django application! Let's do that next.

Working with Products and Plans

To get started we're going to run through a few examples using just Products and Plans. Once that's out of the way we'll move on to Subscriptions.

Creating a Pricing Page

The first thing we might want to do is create a pricing page. This is where our potential customers can see our different tiers, how much they cost, and what's in them. It's also the place where—eventually—they'll be able to subscribe to a plan.

Let's start by setting up the UI.

Since all our data is now synchronized from Stripe, we won't have to go back to Stripe to get the data but can just inspect our local dj-stripe models.

At a very basic level, that might look something like the below.

Note: If the files/terminology below don't make sense, you may need to
brush up on Django views.

1. Set up a URL route

In urls.py:

urlpatterns = [
    path(pricing_page/', views.pricing_page, name='pricing_page'),
]
Enter fullscreen mode Exit fullscreen mode

2. Create the View

In views.py:

from django.shortcuts import render
from djstripe.models import Product

def pricing_page(request):
    return render(request, 'pricing_page.html', {
        'products': Product.objects.all()
    })
Enter fullscreen mode Exit fullscreen mode

Notice how we are just grabbing the Products from our database using the ORM instead of hitting the Stripe API. We can do this because we have synced our Stripe data to our application DB.

3. Create the Template

In pricing_page.html:

{% raw %}
<section>
  <p class="title">Pricing Plans</p>
  <div class="columns">
    {% for product in products %}
      <div class="column">
        <p class="subtitle">{{ product.name }}</p>
        {% for plan in product.plan_set.all %}
          <div>
            <p class="heading">{{ plan.nickname }}</p>
            <p>{{ plan.human_readable_price }}</p>
          </div>
        {% endfor %}
      </div>
    {% endfor %}
  </div>
</section>
{% endraw %}
Enter fullscreen mode Exit fullscreen mode

If you've set things up properly this should render a page that looks something like the below (you'll need to have Bulma CSS on the page for the styling to work).

Basic Pricing Plan Page

Our bare-bones pricing page, using the Product and Plan data from Stripe

Not bad for a few lines of code!

But—we probably want to display a bunch more stuff than just the names and prices.

A real SaaS pricing page might look more like this.

Fancier Pricing Plan Page

A more fleshed-out pricing page, from our live demo.

Here we've added three pieces of information to each tier:

  1. A description/tagline saying more about the tier and who it's for.
  2. A list of features that are available.
  3. Whether the tier is the default—which is highlighted in the UI.

To make this page we're going to need to create some additional metadata around the Products and Plans.

Adding metadata to your Stripe objects

Ok, so we want to attach the above information to our pricing tiers.

How should we do that?

Once again, we are faced with several options:

  1. We could store this information in Stripe. Stripe allows arbitrary "metadata" to be attached to objects, so we could store it there, sync it to our database, and then display it, similar to the other pieces of information.
  2. We could store the information in our application database. Since all of this data is application-specific there's not really a need for it to be in Stripe. So we could just create a local Database model to store it. Then we don't have to use Stripe's clunky metadata UI or worry about sync issues. This seems more appealing.
  3. We could store the information in code. If this data is coupled with our application anyways, we could bypass the database entirely and just keep it in our source code. This simplifies things even more—though comes with the downside of requiring code changes to make changes.

This guide recommends keeping additional metadata in your code.

Why? Well the main reason is that your Django application code is ultimately going to be coupled with this data in some way, so you might as well do it all that way.

Let's look at this by example. In the second pricing page above there's a feature called "Ludicrous Mode" that should only be available on a Premium subscription.

One thing we needed to do is show "Ludicrous Mode" on the pricing page. That could be done easily with all three options above.

But, we also want to have logic that only allows our users to enter Ludicrous Mode if they are subscribed to the right plan.

Unless the entire permission matrix of your application lives in a database (and good luck with that if it does)—you'll end up with code like the following:

if ludicrous_mode_enabled():
  do_ludicrous_stuff()
Enter fullscreen mode Exit fullscreen mode

So invariably your code will be coupled with your pricing tiers, anyway. Therefore, might as well commit to maintaining this logic in code and then at least there's fewer ways for your code and data to get out of sync.

Ludicrous Mode

"We'll have to go right to ludicrous speed."

Keeping this logic in your code also makes it easier to keep your different environments in sync, write automated tests for feature-gating logic, and roll-out and (and rollback) changes to production.

There cons of this setup—the largest being that it require developers and a deploy to production to make any changes—but by and large we've found it to be the simplest and easiest to maintain for small-to-medium sized applications and teams.

So we're going to add some code to augment our Stripe Product data. Here's what that might look like:

@attr.s
class ProductMetadata(object):
    """
    Metadata for a Stripe product.
    """
    stripe_id = attr.ib()
    name = attr.ib()
    features = attr.ib(type=list)
    description = attr.ib(default='')
    is_default = attr.ib(type=bool, default=False)


PREMIUM = ProductMetadata(
    stripe_id='prod_GqvWupK96UxUaG',
    name='Premium',
    description='For small businesses and teams',
    is_default=False,
    features=[
        features.UNLIMITED_WIDGETS,
        features.LUDICROUS_MODE,
        features.PRIORITY_SUPPORT,
    ],
)
# other plans go here
Enter fullscreen mode Exit fullscreen mode
Example of attaching metadata to your Stripe Products in code. This example uses the excellent Python attrs library.

In the above example, we've created a metadata class to associate with a Stripe product and manually linked it by stripe_id. We've added attributes for our description, list of features and (and any other information we want) entirely in code, which allows us to more easily test, version control, and roll out changes that are specific to our application.

We can use this metadata structure to build out our new pricing page.

Here's a sketch of the unstyled HTML for that page, assuming that each of your stripe products has a .metadata property referencing the class above.

{% raw %}
<div class="plan-interval-selector">
  {% for plan in plans %}
    <button class="button">{{ plan.name }}</button>
  {% endfor %}
</div>
<div class="columns plan-selector">
{% for product in products %}
  <div class="column">
    <div {% if product.metadata.is_default %}class="is-selected"{% endif %}>
      <span class="icon">
        <i class="fa {% if product.metadata.is_default %}fa-check{% else %}fa-circle{% endif %}">
        </i>
      </span>
      <p class="plan-name">{{ product.metadata.name }}</p>
      <p class="plan-description">{{ product.metadata.description }}</p>
      <div class="plan-price">
        <span class="price">{{ product.metadata.monthly_price }}</span> / month
      </div>
      <ul class="features">
         {% for feature in product.metadata.features %}
         <li>
           <span class="icon"><i class="fa fa-check"></i></span>
           <span class="feature">{{ feature }}</span>
         </li>
         {% endfor %}
      </ul>
    </div>
  </div>
{% endfor %}
</div>
{% endraw %}  
Enter fullscreen mode Exit fullscreen mode
Richer pricing page example using a ProductMetadata class attached to Stripe data.

This exercise of styling the HTML and making it interactive is left up to the reader.

For a complete working example, check out SaaS Pegasus—the Django SaaS Boilerplate.

Setting up your Subscription Data Models

Phew! Ok, now we've got our Stripe Product and Plan data synchronized with our application and we are using the data—along with some additional metadata—to generate our grid of pricing tiers. It's a good start, but we still haven't done anything to allow our users to purchase and use our Products and Plans. So let's get into that.

The first thing we'll want to do is set up the data models. And like the Products and Plans, we'll follow the same basic principle of making Stripe the source of truth, and then mapping the Stripe data to our application models. Once again we'll take advantage of dj-stripe to handle a lot of the data synchronization.

The basic plan will be:

  1. A user goes through the subscription workflow on our site
  2. We create a subscription object in Stripe
  3. We sync that subscription to our application database
  4. Finally, we attach the subscription to our local data models (e.g. the logged-in acccount)

We'll cover steps 1-3 in depth when we go over creating your first subscription, but first we're going to discuss data modeling.

Choosing how to model Subscriptions in your Django application

As we mentioned above, we'll be focusing on the Subscription and Customer Stripe objects.

So let's assume we already have these objects synchronized to our database. How do these fit in to our application?

To decide this we'll have to answer two basic, but important questions:

  1. What is the unit of data in our application that should be associated with the Subscription object? The answer to this is typically, the primary unit that is associated with the tier itself.
  2. What is the unit of data in our application that should be associated with the Customer object? For this one, the right data model is typically associated with how the tier is managed.

The choice of how to manage these will often be application-specific, though there are a few common use-cases we can cover.

A user-based SaaS (typically B2C)

In a user-based SaaS each person has their own account and manages their own subscription. This is the most common model for business-to-consumer (B2C) apps like Spotify or Netflix (ignoring family plans).

For user-based SaaS applications the answer is likely that the Django User model is the right place to associate both your Subscription and Customer details. Going back to the criteria above, the User is associated with the subscription tier, and manages it too.

Assuming you have overridden the User model (which is highly recommended), that would look something like this:

class CustomUser(AbstractUser):
    customer = models.ForeignKey(
        'djstripe.Customer', null=True, blank=True, 
        help_text=_("The user's Stripe Customer object, if it exists")
    )
    subscription = models.ForeignKey(
        'djstripe.Subscription', null=True, blank=True, 
        help_text=_("The user's Stripe Subscription object, if it exists")
    )
Enter fullscreen mode Exit fullscreen mode
Associating the Subscription and Customer with a CustomUser model, for a typical B2C SaaS application

A team-based SaaS (typically B2B)

Most SaaS applications are actually not consumer-facing, but instead target other businesses. For a business-to-business (B2B) SaaS it's more likely that you'll have the concept of "Teams" or "Organizations" that contain multiple users—typically mapping to a company or division of a company.

In this case you likely want to associate the Subscription with the Team model (or whatever you've named it), because that's the unit that the tier "belongs to".

That might look like this:

class Team(models.Model):
    """
    A Team, with members.
    """
    team_name = models.CharField(max_length=100)
    members = models.ManyToManyField(
        settings.AUTH_USER_MODEL, related_name='teams', through='Membership'
    )
    subscription = models.ForeignKey(
        'djstripe.Subscription', null=True, blank=True,
        help_text=_("The team's Stripe Subscription object, if it exists")
    )
Enter fullscreen mode Exit fullscreen mode
Associating the Subscription with a Team model, for a typical B2B SaaS. In this case all members of the team would have their subscription associated through the Team.

Okay, that makes sense, but in this case where should the Customer association go?

Well, you probably don't want everyone in the Team to be able to modify/cancel the Subscription. That's likely something that only someone who's an administrator of some kind should be able to do.

Once again, there are a couple options.

The simplest one is to associate the Customer with the User object again. This often works, although can create problems in the rare case where someone is using the same User account and administering multiple Teams.

Often, a better option is to use the through model to associate this information with the Team membership. In the Team example above you can see this on the members field.

That Membership model then might look something like this:

class Membership(models.Model):
    """
    A user's team membership
    """
    team = models.ForeignKey(Team, on_delete=models.CASCADE)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    role = models.CharField(max_length=100, choices=roles.ROLE_CHOICES)
    customer = models.ForeignKey(
        'djstripe.Customer', null=True, blank=True,
        help_text=_("The member's Stripe Customer object for this team, if it exists")
    )
Enter fullscreen mode Exit fullscreen mode
Using a through model to create a Customer association with a Team's membership.

Other models and key takeaways

Your application may require even more complicated set ups than this. For example, allowing multiple people to manage a Team's billing would probably have to be associated with a Role object of some kind. Or going back to Spotify—the Subscription might need to be optionally associated with a User or a "Team" (in the case of a family plan).

Ultimately, how you set this up is up to the details of your own application's data models, but the key takeaway is to associate the Subscription and Customer objects with the right application models according to this general principle:

The Subscription object should be attached to the data model associated directly with the pricing tier and the Customer object should be associated with the data model associated with who manages the pricing tier.

If you follow that rule you should be good. Also, this guide recommends starting with the simplest model that works, and only expanding it when needed. You know, YAGNI and all.

For the rest of this example we'll use the B2C use case where the Subscription and the Customer are both attached to our custom Django User model.

Creating your first Subscription

Now that we've figured out how to model our data we can finally wire everything up. Time to finally get paid!

Thankfully Stripe already provides an incredible guide on setting up a Subscription that handles a lot of the details for us. This section will walk through that guide, providing details specific to our Django project, and focusing mostly on the sections tagged "server-side".

We've already completed Step 1 by creating our Products and Plans above.

For Step 2, the Stripe Python package should have been installed already via the dj-stripe dependency—though if you use a requirements file it's good to explicitly add the stripe package there since it is a true dependency.

Next, follow Step 3 and Step 4 of the Stripe guide to collect and save card details on the front-end.

These steps do not have any backend-specific dependency and you can follow the instructions from Stripe basically as-is. However, we recommend (and assume) that the email you pass to Stripe is the same as the logged-in User's email, which you can grab in the Django template using {% raw %}{{ user.email }}{% endraw %}.

Submitting payment information to your server (client-side)

This corresponds to Stripe's Step 5.

Assuming you've completed Steps 1-4, it's time to submit the newly created payment information to our server from the front end. We'll build off Stripe's example, but make a few changes.

Here's the basic JavaScript code—with the assumption that it has been written in the context of a Django template.

{% raw %}
  const createCustomerUrl = "{% url 'subscriptions:create_subscription";
  function stripePaymentMethodHandler(result, email) {
    if (result.error) {
      // Show error in payment form
    } else {
      const paymentParams = {
          email: email,
          plan_id: getSelectedPlanId(),
          payment_method: result.paymentMethod.id,
      };
      fetch(createCustomerUrl, {
        method: 'post',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRFToken': getCookie('csrftoken'),
        },
        credentials: 'same-origin',
        body: JSON.stringify(paymentParams),
      }).then(function(response) {
        return response.json(); 
      }).then(function(result) {
        // todo: check and process subscription status based on the response
      }).catch(function (error) {
        // more error handling
      });
    }
  };
{% endraw %}
Enter fullscreen mode Exit fullscreen mode

Let's walk through this in detail.

First we grab the URL of the customer/subscription creation URL that we're going to submit the data to using the Django {% raw %}{% url %}{% endraw %} tag.

{% raw %}
  const createCustomerUrl = "{% url 'subscriptions:create_subscription";
{% endraw %}
Enter fullscreen mode Exit fullscreen mode

We'll define this URL in the next step, but for now just assume it exists.

Next we define the stripePaymentMethodHandler function and error handling. This bit is the same as the Stripe guide.

function stripePaymentMethodHandler(result, email) {
    if (result.error) {
      // Show error in payment form
    } else {
      // create customer and subscription
    }
Enter fullscreen mode Exit fullscreen mode

Then it's time to create the customer—via a POST request to our backend.

Here we're going to make a few changes to the Stripe example—bringing in Django best-practices, and modifying the code to allow creating the Customer and Subscription object in a single request.

Here's the code.

      // create customer and subscription
      const paymentParams = {
          email: email,  // 1. assumed to be the logged-in user's email from Step 4
          plan_id: getSelectedPlanId(),  // 2. get the selected plan ID from your DOM / state
          payment_method: result.paymentMethod.id,
      };
      fetch(createCustomerUrl, {  // 3. use the url variable we defined earlier
        method: 'post',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRFToken': getCookie('csrftoken'),  // 4. CSRF validation support
        },
        credentials: 'same-origin',
        body: JSON.stringify(paymentParams),
      }).then(function(response) {
        // processing and error handling
      });      
Enter fullscreen mode Exit fullscreen mode

A few notable changes that have been made from the Stripe version:

  1. We've continued the assumption that the Stripe subscription was set up with the logged-in User's email.
  2. Since we're also going to create the Subscription in this request, we need to pass a plan_id to our backend (to know what plan to subscribe the customer to). In this example we've made the assumption that the plan_id is available from the DOM / state of your application and abstracted those details to the getSelectedPlanId helper function. This would be implemented by you according to how you've structured your pricing page.
  3. Rather than hard-coding it, we've grabbed our createCustomerUrl from the variable set by the {% raw %}{% url %}{% endraw %} tag above. 1. We've added the X-CSRFToken header by pulling the cookie from Django's built-in Cross-Site Request Forgery (CSRF) protection. The Django docs on CSRF protection have more details on this approach, as well as the source of the getCookie function.

Ok, that wraps the front-end bits required to create the Customer and Subscription.

Now onto the backend!

Creating the Customer and Subscription Objects (server-side)

This corresponds to Stripe's Step 6 and Step 7.

Here we'll define the backend views to create the Customer and Subscription objects, as well as associate them with the application models we chose above.

First we'll first create a URL for the endpoint.

In your app's urls.py:

from django.urls import path
from . import views

app_name = 'subscriptions'
urlpatterns = [
    # other URLs go here
    path('create_subscription/', views.create_customer_and_subscription, 
         name='create_subscription'
    ),
]
Enter fullscreen mode Exit fullscreen mode

The app_name and url name should match what we used in the {% raw %}{% url %}{% endraw %} tag on the front-end.

Next we'll create the view in views.py. There's a lot going on here so we'll walk through it in detail but here's the complete view to start.

@login_required
@require_POST
@transaction.atomic
def create_customer_and_subscription(request):
    """
    Create a Stripe Customer and Subscription object and map them onto the User object
    Expects the inbound POST data to look something like this:
    {
        'email': 'cory@saaspegasus.com',
        'payment_method': 'pm_1GGgzaIXTEadrB0y0tthO3UH',
        'plan_id': 'plan_GqvXkzAvxlF0wR',
    }
    """
    # parse request, extract details, and verify assumptions
    request_body = json.loads(request.body.decode('utf-8'))
    email = request_body['email']
    assert request.user.email == email  
    payment_method = request_body['payment_method']
    plan_id = request_body['plan_id']
    stripe.api_key = djstripe_settings.STRIPE_SECRET_KEY

    # first sync payment method to local DB to workaround 
    # https://github.com/dj-stripe/dj-stripe/issues/1125
    payment_method_obj = stripe.PaymentMethod.retrieve(payment_method)
    djstripe.models.PaymentMethod.sync_from_stripe_data(payment_method_obj)

    # create customer objects
    # This creates a new Customer in stripe and attaches the default PaymentMethod in one API call.
    customer = stripe.Customer.create(
      payment_method=payment_method,
      email=email,
      invoice_settings={
        'default_payment_method': payment_method,
      },
    )
    djstripe_customer = djstripe.models.Customer.sync_from_stripe_data(customer)

    # create subscription
    subscription = stripe.Subscription.create(
      customer=customer.id,
      items=[
        {
          'plan': plan_id,
        },
      ],
      expand=['latest_invoice.payment_intent'],
    )
    djstripe_subscription = djstripe.models.Subscription.sync_from_stripe_data(subscription)

    # associate customer and subscription with the user
    request.user.customer = djstripe_customer
    request.user.subscription = djstripe_subscription
    request.user.save()

    # return information back to the front end
    data = {
        'customer': customer,
        'subscription': subscription
    }
    return JsonResponse(
        data=data,
    )
Enter fullscreen mode Exit fullscreen mode

Ok, let's walk through that in sections, starting with the declaration:

@login_required
@require_POST
@transaction.atomic
def create_customer(request):
    # body
Enter fullscreen mode Exit fullscreen mode

You can see we are requiring a login for this view. That's because we are going to use the logged-in user to determine who associate the subscription with. We're also requiring a POST since that's what the front-end API should always do, and we're wrapping everything in an @atomic transaction, so we don't end up with our database in a partially-committed state.

Next we extract the data from the body of the POST and validate our assumptions:

    request_body = json.loads(request.body.decode('utf-8'))
    email = request_body['email']
    assert request.user.email == email  
    payment_method = request_body['payment_method']
    plan_id = request_body['plan_id']
Enter fullscreen mode Exit fullscreen mode

In the last step we passed the POST data to the backend as a JSON stringified set of data, so we extract that and then pull out the individual parameters. We also double-check our assumption that the email from the form match the logged-in User.

Note: It's not required that the email in Stripe match the User's email, but it will make searching for data in the Stripe Dashboard easier.

Next we initialize Stripe with the api_key from our settings.py.

    stripe.api_key = djstripe_settings.STRIPE_SECRET_KEY
Enter fullscreen mode Exit fullscreen mode

We then sync the PaymentMethod object locally (note, this is just to workaround this bug in dj-stripe).

    payment_method_obj = stripe.PaymentMethod.retrieve(payment_method)
    djstripe.models.PaymentMethod.sync_from_stripe_data(payment_method_obj)
Enter fullscreen mode Exit fullscreen mode

Next we create the Customer object in Stripe, using the Python API. This part is similar to what's found in Step 6 of the Stripe guide.

    # Create a new Customer in stripe and attach the default PaymentMethod in one API call.
    customer = stripe.Customer.create(
      payment_method=payment_method,
      email=email,
      invoice_settings={
        'default_payment_method': payment_method,
      },
    )
Enter fullscreen mode Exit fullscreen mode

Now the customer has now been created in Stripe, but we still need to sync it to our local application DB which we can do with this line using the sync_from_stripe_data helper function available on every dj-stripe model.

    djstripe_customer = djstripe.models.Customer.sync_from_stripe_data(customer)
Enter fullscreen mode Exit fullscreen mode

Now that we have a Customer we can create and sync the Subscription object in a similar way.

    # create subscription
    subscription = stripe.Subscription.create(
      customer=customer.id,
      items=[
        {
          'plan': plan_id,
        },
      ],
      expand=['latest_invoice.payment_intent'],
    )

    # and sync it to our application DB
    djstripe_subscription = djstripe.models.Subscription.sync_from_stripe_data(subscription)
Enter fullscreen mode Exit fullscreen mode

And then we can finally associate the new Customer and Subscription objects with our logged in User.

    request.user.customer = djstripe_customer
    request.user.subscription = djstripe_subscription
    request.user.save()
Enter fullscreen mode Exit fullscreen mode

With that done, all that's left is to return some data to the front-end so that the next steps can be taken.

    data = {
        'customer': customer,
        'subscription': subscription
    }
    return JsonResponse(
        data=data,
    )
Enter fullscreen mode Exit fullscreen mode

If everything went well, our first Subscription should be created and associated with our logged in User!

Managing the Subscription Status (client-side)

This corresponds to Stripe's Step 8.

At this point we've created the Customer and Subscription objects and associated them with the appropriate User. So what's left?

Well, unfortunately, we aren't yet guaranteed that they've been created successfully. In many cases—the most common being a 3D-Secure workflow—there will be additional authentication steps required.

This part can follow the Stripe guide almost verbatim. Just insert the code from Stripe's guide into your front-end above where we wrote this.

// todo: check and process subscription status based on the response
Enter fullscreen mode Exit fullscreen mode

The only thing you'll need to modify is extracting the subscription variable details from the backend response. That looks something like this.

  const subscription = result.subscription;
  const { latest_invoice } = subscription;
  // continue with the rest of the Stripe example code
Enter fullscreen mode Exit fullscreen mode

If you've made it this far you just created your very first Subscription. Congrats!

That's it for the current version of this guide. However, in the future I plan to flesh out additional sections on using Webhooks to keep Subscription data in sync, and working with Subscription objects in your Django application.

If there's any other content you'd like to see added, please reach out to cory@saaspegasus.com and let me know!

For a head start building your Django SaaS application check out
SaaS Pegasus—the Django SaaS Boilerplate which comes with subscriptions, teams and a whole lot more right out of the box!

This guide was originally published on saaspegasus.com

Top comments (19)

Collapse
 
nyamador profile image
Desmond

Amazing Post Cory.
I've been trying to figure out multi tenant Saas applications with Django . But my one question has always been with infrastructure. Does this mean that every new instance of the multitenant saas app spins up a new server instance or kubernetes cluster?

Collapse
 
czue profile image
Cory Zue

Thanks Desmond!

Re multi-tenancy, it depends on how you architect it. This book is a good starting point. If you go with one of the simpler options like the shared database / shared schema approach then you don't need to worry about infrastructure. But if you go all the way to "completely isolated tenants using Docker" then yeah - you need to do quite a lot of devops to get things working!

Collapse
 
simo97 profile image
ADONIS SIMO

Awesome, thank for the book recommandation also.

I have similar issue but with database design. Actually i am separating tenant via schemas and i am using AWS i feel like with this GDPR law i will have to store "EU tenant" informations in a database within an EU region but i have no clue of how to do this separation (knowing that there is some informations in the shared tenant ).
Any idea please ?

Thread Thread
 
czue profile image
Cory Zue

Hmm, that's not something I have much experience with. I imagine you could "fork" the tenant at the app-level (e.g. set up a clone at eu.myapp.com) or the DB-level (where I guess you'd have to route app-layer traffic based on a chosen user setting), with varying tradeoffs.

The former has the advantage of having the whole backend be colocated and in the EU which should help with performance, but comes with the operational overhead of maintaining two sites. The latter would maybe be a simpler set up for the end-user but introduce more latency behind the web server if you're making DB requests across an ocean.

Hope that helps!

Thread Thread
 
simo97 profile image
ADONIS SIMO

Yeah thank you. I will dive into those path to know more.

Collapse
 
nyamador profile image
Desmond

Awesome!
I really appreciate your help🙏

Collapse
 
ohduran profile image
ohduran

Great post, I've learnt a lot!

Maybe the first step before integrating that is having an MVP ready? If you know your way on Docker and have some React knowledge, I've built a cookiecutter that allows you to jumpstart the whole SaaS business and deploy it to Heroku in a breeze. Check it out here: github.com/ohduran/cookiecutter-re...

Collapse
 
czue profile image
Cory Zue

Thanks ohduran!

You're definitely right that you'd want some kind of MVP app ready before trying to get anyone to pay for it. :)

Thanks for sharing your template! I'm a big fan of cookiecutter!

Collapse
 
steelwolf180 profile image
Max Ong Zong Bao

Wow, you literally spend tons of time to do this is really awesome article.

Collapse
 
czue profile image
Cory Zue

Haha, thanks! Yeah I probably spent 40+ hours on this... :)

Collapse
 
steelwolf180 profile image
Max Ong Zong Bao

Wow massive & important work, I wanted to write something on Django and e-commerce. Which was super overwhelming for me when I look for it like people who are using wagtail for it.

Thread Thread
 
czue profile image
Cory Zue

Thanks Max! I'd love to see that! I've done Django+ecommerce, but mostly just selling one or two products on my own sites, nothing complex or at scale.

Collapse
 
ssijak profile image
Saša Šijak

Good post on the technical side.

But, regarding Stripe, after all the pain of integrating it, you discover that you have to handle tax hell in some way, and as a solo developer or a small team for a new SaaS that is almost unmanageable. And than you either break the law and hope for the best, or move to something like Paddle.

Tax Hell = Needing to charge VAT for all customers from Europe and pay it back to respective countries, but there are nuances and exceptions. Some other countries have other schemes. And US has tax nexus laws per state.

Collapse
 
simo97 profile image
ADONIS SIMO

Interesting point this is something we don't really look for when we do this payment stuff on website.
(tutorial's authors didn't spoke about that, lol)

Collapse
 
agenteand profile image
Luis Solis • Edited

Amazing post.
When will we see more posts?
I do not know if I missed something, but it is not clear how you attach ProductMetada class to stripe plan.
I'm waiting for: Feature-gating (restricting access to content based on a user's Subscription)

Thanks Cory

Collapse
 
simo97 profile image
ADONIS SIMO • Edited

One more time, thanks a lot for this, it come at the RIGHT time as i am working on a such platform with Django, but i had to re-write the example to work in REST (DRF) context and React.js.

Now i am wondering, what type of subscription is good if i want to price my SaaS per user per month ?

I saw metered , fixed-price and per-set but i am not sure which one will be the right. i saw them here: stripe.com/docs/billing/subscripti...

Collapse
 
paqman85 profile image
Glenn Paquette

Amazing Corey! This is just such an amazing post. :)

Collapse
 
heyfirst profile image
First Kanisorn Sutham

Thanks for sharing Cory!

Collapse
 
techt01ia profile image
Techtolia

If you are looking the latest solution of Stripe subscription integration in ASP.NET, check out the demo >>> techtolia.com/StripeSubscriptions/