Using Stripe for recurring monthly payments to a paid SaaS subscription in a Django + Vue.js application
This diagram shows the flow of data for the lifecycle of a paid customer subscription in a Django application with a Vue.js client. There are four stages:
I. Account setup, configuration, model and object creation II. Logic and data flow for starting a customer's premium monthly subscription III. Automatic subscription renewal IV. Cancelling a premium subscription
This is my first attempt at using Stripe, or any other online payment service API. Most of what I have diagramed here comes from this article from the Stripe documentation: https://stripe.com/docs/billing/subscriptions/fixed-price. Knowing almost nothing about what is needed to create a SaaS subscription, I found this article very helpful. It was a lot to read at once, but each call to to the Stripe API is very clear and straightforward.
I made some modifications and additions to this walkthrough for my use case, which is an API service called Open SEC Data, an open source project that I'm working on (https://gitlab.com/briancaffey/sec-filings-app).
Here's a read-only link to the diagram: https://drive.google.com/file/d/1oH2b0W-c-dI5oXzc_jvCGvXx9sJagr4a/view?usp=sharing. This diagram is made with https://www.diagrams.net/.
Here's a detailed description of each part of the diagram, starting with the first section.
-
Setup a Stripe account. For local development, make sure you turn on
View test data
. On your local machine, install the stripe CLI and authenticate with your Stripe account -
Create a Product in Stripe (mine is called
Open SEC Data Premium Subscription
) -
Create a Price in Stripe that references the Product.
Instead of creating these objects in the Stripe Dashboard, you can also create them with the Stripe CLI or the Python SDK. I created a Django management command called
create_stripe_data
that will create a Product and related Price in Stripe. We will need the id of the Price, it looks like this:price_1Hx0goL67dRDwyuDh9yEWsBo
. -
Add the Price ID as an environment variable
SUBSCRIPTION_PRICE_ID
to the backend. This will be used later when we make API calls to Stripe from inside of Django views. -
For production environments, you will need to a register a Stripe webhook. This is an endpoint in Django that Stripe will POST to in order to inform the Django application of events that have happened in Stripe.
For local development we need to run
stripe listen --forward-to localhost/api/stripe-webhooks/
in order to forward webhook events to the local Django application. This works really well for local development. -
In both local and production environments we need to add an environment variables to the Django application that will be used to validate the webhook event.
-
You will need to create a
Subscrtiption
model or similar in your Django models. This model should be related to your user model in some way. At a minimum it should have thesubscription_id
(the ID of the Stripe Subscription) andcurrent_period_end
(also from the Stripe Subscription object). We will use this model in the next sections. -
/api/stripe-webhooks/
is the endpoint in the Django application that Stripe will send POST requests to in order to inform the Django application of events that happen in Stripe. The URL can be called anything you want, as long as you register it with that URL. In local development, you need to specify this URL in thestipe listen
command (for example,stripe listen forward-to localhost/api/stripe-webhooks/
). -
STRIPE_SECRET_KEY
is the name of the secret API key that should only be accessible by the backend. In local development, this key looks likesk_test_Abc123
. In production, this key will look likesk_Abc123
. -
stripe
is the name of the PyPI package that we need to add torequirements.txt
(requirements/base.txt
). -
STRIPE_PUBLISHABLE_KEY
is the value of the Stripe API key that can be made public and is used in the Vue application to instantiate Stripe. -
The Stripe library is included in
index.html
via CDN so that it is accessible anywhere in the Vue application. Stripe object is instantiated in the Vue application with:let stripe = Stripe(process.env.STRIPE_PUBLISHABLE_KEY)
With everything setup and configured properly in Stripe, the backend Django application and the frontend Vue application, customers can now start paying for monthly subscriptions. In my application, a user can sign up for an account first without having a premium subscription. In other scenarios, having an active account may require a premium subscription.
-
When a logged-in user visits their
/account
page, they will see the status of their account: Basic (free) or Premium (paid subscription). Users on a Basic plan will see the option to upgrade to Premium. They will be redirected to a/premium
page where they will be presented with a credit card form. This credit card form is generated by Stripe Elemenets. The user fills out their credit card, expiration date, card security code and billing ZIP code and then clicksPurchase
. -
Clicking on
Purchase
calls a methodpurchase
that callsstripe.CreatePaymentMethod
. ThepaymentMethodId
token returned fromstripe.CreatePaymentMethod
is then passed to the method calledcreateSubscription
. -
Stripe creates this object and returns a response that contains a
paymentMethodId
token. -
createSubscription
sends a POST request to/api/stripe/create-subscription/
in the Django application with thepaymentMethodId
that we generated in the previous step. -
/api/stripe/create-subscription/
calls a view calledcreate_subscription
which makes a number of API calls to Stripe and then finally saves some data in the application's Postgres database. -
The first API call creates the Customer object in Stripe if it does not exist.
email=request.user.email
is used in the API call to associate the Stripe customer with the user's email. -
Next the payment method is attached to Stripe Customer model.
-
Next the Stripe payment method is set as the default payment method for Stripe customer for future billing.
-
The Stripe subscription model is created with the customer ID that was created in the earlier and the price ID corresponding to the premium subscription (added in the setup stage).
-
Once these Stripe API calls have finished, a new Subscription is saved in the Postgres database.
stripe_subscription_id
,stripe_customer_id
andvalid_through
(DateTimeField that keeps track of the date through which the user's subscription has been paid for) are saved to theSubscription
model and then the subscription model is saved to the user model'ssubscription
field. -
When the
createSubscription
method's POST requests returns successfully, the user's account is fetched again from/api/account/
-
The browser makes a request to
/api/account/
. -
/api/account/
returns information on the user and their subscription. -
Data from
/api/account/
is updated in Vuexuser
store. -
The user is now able to make requests to resources for premium features.
-
In this applicatoin, one such example is the ability to request an API key for making for making API calls.
-
A user makes a request to an API endpoint for a premium feature.
-
When determining permissions for resources that should only be accessible to customers with valid subscriptions, we need to compare
request.user.subscription.valid_through
totimezone.now()
and make sure thatvalid_through
is greater thantimezone.now()
. -
Requests for protected resources are successfully returned to the browser.
The customer's credit card is charged once each montht that they are subscribed to the service. This action happens in Stripe. This section assumes that the customer's primary payment method is still valid (it has not been canceled experied or not able to be charged for some other reason).
-
The customer's card is charged in Stripe and an event is sent to the Django backend via a webhook that we registered in the setup stage.
-
The webhook view checks
event.type
and if the event is of typeinvoice.paid
we extend the user's subscription by one month. -
To extend the user's subscription, we modify the
DateTimeField
field on theSubscription
that tracks thecurrent_period_end
which is included in the webhook data object. The model field in my code is calledvalid_through
.
-
When a user decides to cancel their payed subscription service, they click on the
Cancel My Subscrtiption
button. -
This makes a POST request to
/api/stripe/cancel-subscription
which calls thecancel_subscription
view. This view callsstripe.Subscription.delete(subscriptionId)
, where thesubscriptionId
is retreived fromrequest.user.subscription
(theSubscription
model created in the setup section). -
The subscription is deleted in Stripe through the
stripe.Subscription.delete
API call. -
The user's subscription is deleted from the user model with
request.user.subscription.delete()
. -
The frontend responds to the deleted subscription by fetching
/api/account/
again, refresh, or redirecting and the user no longer has access to their premium subscription.