Blog | Tristan Kernan

“That some of us should venture to embark on a synthesis of facts and theories, albeit with second-hand and incomplete knowledge of some of them – and at the risk of making fools of ourselves” (Erwin Schrödinger)

Django: dj-stripe

I spent the last week integrating Stripe into a side project. Given the popularity of the library, I expected dj-stripe to simplify the implementation and save me time. Internally, I bucketed a few hours to a complete implementation. Boy, was I wrong.

Background

For context, my side project is a metered api gateway. I offer a free tier and a monthly subscription tier. My basic use cases are:

When a user signs up for a subscription, they'll be upgraded to the higher tier internally. Likewise on cancellation, they'll be downgraded.

What does dj-stripe do, anyway?

I expected dj-stripe to integrate cleanly with a Django application, through webhook handling and customer object relations. While the library does provide these functions, what I didn't realize is that dj-stripe does so much more: it actively syncs your stripe account with your database. Every product, price, transaction, subscription, tax zone, etc., etc. is sync'd into your database. This is purportedly for efficiency: rather than fetching data from the stripe api, it can be pulled in via a db query. I recognize that this is a real performance issue, at a previous company some endpoints serially queried the stripe api multiple times, leading to multi-second response times on critical paths.

For a side project of my scale, though, in hindsight it was definitely overkill. I have one product, and one price; customers can be created via stripe api; subscription status can be managed through webhooks.

Documentation

The dj-stripe documentation is, bluntly, awful. It somehow fails to describe how to do much of anything that I was interested in doing; I imagine that SaaS subscription billing is common among Django developers! Where the documentation does exist, it's misleading if not outright wrong. Even popular blog posts, like How to Create a Subscription SaaS Application with Django and Stripe are confusing and lack details.

As an example, let's look at the demo code for stripe checkout. It's more than a hundred lines, and manually associates Customer and User objects. Compare to my own implementation (courtesy of claude):

Python
class SubscribeCheckoutRedirectView(LoginRequiredMixin, View):
    def post(self, request, **kwargs):
        customer, _ = Customer.get_or_create(subscriber=request.user)

        success_url = (
            self.request.build_absolute_uri(reverse("dashboard")) + "?subscribed=1"
        )
        cancel_url = self.request.build_absolute_uri(reverse("dashboard")) + "?bailed=1"

        session = stripe.checkout.Session.create(
            customer=customer.id,
            success_url=success_url,
            cancel_url=cancel_url,
            mode="subscription",
            line_items=[
                {
                    "price": settings.STRIPE_HOBBY_PRICE_ID,
                    "quantity": 1,
                }
            ],
        )

        return redirect(session.url)

A fraction of the code - and the only usage of dj-stripe is in Customer.get_or_create(), the resulting customer id automatically associating the session/transactions with the internal user.

Configuration

Which leads me to api key management in dj-stripe. This singlehandedly cost me at least an hour of time - unheard of for configuring an api key! The project is apparently migrating towards supporting multiple stripe accounts in a single application (despite nobody asking for it, as far as I can tell). So the documentation says don't put your stripe api keys in django settings, put the keys in the database - but guess what, some parts of dj-stripe still do expect the api key in django settings! So the api key has to be configured there, too. Under what name? Well, there's STRIPE_SECRET_KEY, STRIPE_LIVE_SECRET_KEY, STRIPE_TEST_SECRET_KEY... seriously, why not have one page to describe all configuration settings, with deprecations and recommendations clearly marked?

And why on earth am I storing my stripe api keys in my database!? That feels gross from a security perspective.

Webhooks

Finally, after all of the above, I'm able to actually sit down and build the key piece of functionality: webhook integration. Ah, this has also been rewritten, much to the community's chagrin; it's now exposed via django signals. Thankfully this was relatively simple to build, though the docs for most tutorials and guides are outdated since the rewrite.

Dj-stripe actually manages the webhook for you, creating it via django admin. The use of random urls for the webhook confused me; is that all the security provided? I looked into the code and the database and found that dj-stripe does verify the webhook signature, with the webhook secret stored in the database. Why even bother with the random urls then? Tsk, tsk.

Wrap up

My take is that dj-stripe is trying to do too much, with too little resources. The lack of documentation, the botched api key configuration migration, the multi-account use case.. I think there's a market for a significantly streamlined django + stripe integration for SaaS products, more opinionated in the sense of keeping the feature set small (with an escape hatch when outgrown), and less opinionated in the sense of standard practices like api key management.