- Notes on Solopreneurship Engineering
- Posts
- Issue #12: Integrating Stripe for subscriptions management š¤
Issue #12: Integrating Stripe for subscriptions management š¤
Matching the user onboarding with the Stripe subscription lifecycle
This week has been fully focused on building, so this issue is going to be way more technical than usual. šØ
Brace yourself! š
There are many ways you can integrate Stripe for your SaaS, and today I will share how I tackled it myself.
Table of Contents
Usersā onboarding and plans management
Letās start by understanding how the onboarding is implemented. I discussed it already both here and here, but this issue will focus more on the technical side of it.
This is going to be a great documentation for myself as well once Iāll face this again! š
Beta phase and invite-only beta users
First of all, both the API and the web app have access to a flag named BETA_PHASE_ACTIVE
that indicates whether the product is still in beta or not.
As you can imagine, this flag changes completely how the onboarding works.
When you go to the application during this phase, youāre redirected to the sign-up/in page. Once logged in you will be welcomed and blocked by a screen that says: āOops! We're still working hard!ā.
How can beta users start using the application then? š¤
The database has a profiles
table and each user has a corresponding entry. One of the columns is a flag is_enabled
which is false
by default. The web application simply shows the blocking screen whenever a user is not enabled.
Initially, I was enabling users manually one by one by asking them to ping me once they created their account, but it was adding friction to the onboarding. Hence I implemented a very simple mechanism to automatically enable beta users.
The invitation to a beta user is a URL that looks like: https://app.useechowords.com/?invitation-key=<redacted>
When the web app calls the API for creating the user, it also provides the invitation key. If the provided invitation key matches the expected one, then the newly created user is automatically enabled. š
Ideally, youād want to have unique invitation keys, but that was an overkill. š
A beta user is also associated with a āfakeā subscription which, in turn, is associated with a plan called āBetaā that never expires. In the following sections, Iāll cover more in-depth plans and subscriptions.
Generally available phase
This phase will start the day of the launch which has not been announced yet.
The BETA_PHASE_ACTIVE
flag would be set accordingly and the user signing up would cause a profile to be created and enabled automatically. The other difference is that in this case no subscription is created, and the web app in case of no subscriptions associated with the profile, redirects to an onboarding page.
The onboarding page is simply a pricing table that is integrated with Stripe. Once a plan is chosen and the checkout is successful, a new subscription is created, and the user is given access. š
In the āSubscriptionsā section the user will be able to see again the pricing table and manage their plan.
The āSubscriptionā section of the application
In the following sections, Iāll explain in more detail the integration with Stripe.
Migrating beta users
During the āgenerally availableā phase, beta users wonāt be impacted as soon as their subscription is valid. Before invalidating a subscription, which can be done manually by changing the ending date in the database, Iāll reach out to each beta user.
I promised them a discount for providing feedback, so Iāll share a special offer for each plan through a Stripe payment link. āØ
Once a subscription is canceled, only the āSubscriptionā page will be usable.
Representing subscriptions in the database
Letās now see how Iām representing a plan and a subscription in the database.
Thereās a plans
table and each row corresponds to a specific combination of plan and billing interval. For example, given a āBasicā plan and two possible billing options, yearly and monthly, there would be two different rows in the plans
table.
This is the schema of the plans
table:
Schema of the āplansā table
This means that unless I change pricing or add new plans, this table is going to be populated only once. This also includes a placeholder plan corresponding to the āBetaā plan.
So now we have a profiles
table and a plans
table. The subscriptions
table is the table that links them. There are different scenarios, the ones that will occur during the GA phase are:
a user doesnāt have any subscription: a new user who joined during the GA phase and didnāt proceed with the onboarding,
a user with a subscription that is linked to the āBetaā plan: a beta user who accepted the invitation during the beta phase,
a user with only an active subscription: a new user who joined and onboarded during the GA phase,
a user with two subscriptions, but only one active: a beta user that has its first subscription expired and onboarded with a new one.
This is the schema of the subscriptions
table:
Schema of the āsubscriptionsā table
Syncing Stripeās subscriptions lifecycle
Okay, so now I have a way to represent a plan and a subscription on my side, but how do they link to the corresponding Stripe objects? š¤
If you checked the schema you might have noticed that both have a column called external_{plan,subscription}_id
. Thatās the id of the corresponding Stripe object! š”
All the updates happening on Stripe are notified in real-time to a webhook that is exposed in the API. Itās awesome that Stripe offers a way to test the webhook locally. š¤©
The Stripe API documentation is really well done! Iām going to focus on 4 objects:
Product,
Price,
Subscription,
Payment Link.
The āProductā is your offer or your plan. In my case, I have 3 products: āBasicā, āProā, āPremiumā.
Each āProductā can have multiple āPriceās. In my case, for each product, I have 2 prices: a monthly one, and a yearly one.
The āSubscriptionā can have multiple āPriceās. In my case, each subscription can have only 1 āPriceā. In more complex scenarios a āSubscriptionā could be associated with a recurring price and other stuff depending on the complexity of the offer.
The āPayment Linkā is simply an object with a URL that shows the checkout for a specific āPriceā. Each CTA of the pricing table redirects to the corresponding āPayment Linkā.
Whatās the relationship between these 4 objects and the 2 tables? The plans
table loosely corresponds to the āPriceā objects and the subscriptions
table to the āSubscriptionā objects.
How subscriptions work in Stripe
Iāll describe only what is necessary for managing what I needed, for the rest you can read more about subscriptions here.
A subscription goes through multiple statuses, the ones that Iām tracking are:
active
,past_due
,canceled
.
A new user signs up, and a profile is created. Once the user picks a plan in the pricing table and successfully checks out, an checkout.session.completed
event is sent to the webhook.
This event contains all the information that I need including the āSubscriptionā and the āPriceā object. At this point, I can create an entry in the āsubscriptionsā table, and associate it to the user.
As I mentioned above, once an active subscription is attached to the user, the web app will make the user move forward by granting access. š
Every time a subscription is renewed an customer.subscription.updated
event is sent to the webhook. This makes it possible to ensure that the status is aligned. One important thing is that upon renewal, the subscription object is always the same with the very same id.
Stripeās Customer Portal for managing a subscription
If the payment fails, the status of the subscription sent to the webhook is past_due
. This makes it easy to reflect this change in the subscriptions
table and block users from using the platform. ā
Stripe will automatically retry the payments according to some rules that you can set in the dashboard, once all the retries will fail, an customer.subscription.deleted
event is sent to the webhook. This event will change the status to ācanceledā. At this point, thereās no going back for that subscription, the user would need to start a new checkout. This is the case where Iād have two subscriptions associated with the same user (excluding the case of beta users): an active one and a canceled one.
Users reconciliation
2 users successfully checked out, and you receive 2 checkout.session.completed
events. How do you know which one corresponds to whom? š
This is called āreconciliationā.
As mentioned above, each CTA in the pricing table redirects to the corresponding checkout, and the URL is given by the āPayment Linkā Stripe object. This payment link can be extended by providing a query param client_reference_id
.
Hence, each user will have the URLs specific to them with their own specific reference id. In my case, Iām using a unique identifier of the user in the profiles
table. This value is then available when you receive the checkout.session.completed
event so itās possible to associate the subscription to the correct user.
Creating the product plans in Stripe
I started simply by using the Stripe dashboard. This was the best way to get started, understand how everything is linked, and explore the events in the webhook, etc. Itās great that Stripe has also a ādevelopmentā mode where you can mess up however you want to explore and you also have a handy button to just reset it completely. š
Once I understood what I needed, I just didnāt want to rely on me manually copying to production the same products, prices, and whatever else. There should be a button to ācloneā what you have from ādevelopmentā to āproductionā, but still. š
The thing is that billing is definitely something that I donāt want to fuck up. š¤£
For this reason, I implemented a simple script that takes as input the definition of my āProductās and āPriceās and creates them in Stripe using the API.
Webhook async implementation
Stripe expects your webhook to respond fast. Otherwise, Stripe would consider it as a timeout, and youād mess up with the events. Iād rather not be in that situation. š
Yeah I know, I donāt have paying users yet, so Iām overthinking things that I might not even bump into. Still, billing is really something that Iād rather not ship buggy, even in the first launch. For this reason, Iāve been super careful in implementing this and Iām still covering all the possible scenarios (new users, beta users to migrate, etc.).
The requirement of being fast means that you should return a 2xx status code asap and all the operations on the database should be done async.
Pricing table component
Stripe offers a built-in pricing table that you can just embed as it is. But I donāt like it. š In particular, when you toggle from monthly to yearly it doesnāt show the monthly cost, but only the total yearly cost, so itās harder to see the benefit of the yearly plan.
For this reason, I ended up implementing my own. My implementation toggles between monthly and yearly plans, and also shows available promos.
Pricing table component
DaVinci Resolve
What does this has to do with all the rest?
DaVinci Resolve is a video editing tool. Iām not really into this space, so it might not be the right way to describe it, let me just copy here the description from the website:
DaVinci Resolve is the worldās only solution that combines editing, color correction, visual effects, motion graphics and audio post production all in one software tool!
Thinking about the launch, I started wondering whether a cool video teaser might be beneficial rather than just a loom video. š
In my (little) spare time, Iām also exploring this. š
Conclusion
Uffā¦ Writing this was quite some work. š
This might not be the BEST solution as Stripe is HUGE, but it seems that itās working well.
Funny enough this morning I was thinking āI donāt know what to write today as Iāve been in fully building modeā. But in the end, thereās no reason why I shouldnāt write about this stuff as well. š
Iām now making sure that all the scenarios are covered, and I also need to make sure that tracking the feature usage is still working. Last week I described how I implemented the tracking, but it has been implemented with some assumptions that turned out to be wrong once I started linking with Stripe objects. š
I already implemented somehow the Stripe integration for NextCommit, but it was poorly done, thatās why I decided to start from scratch. This time, all this work will definitely be part of my own boilerplate.
The goals for the end of next week:
be ready for the launch from an implementation perspective (test all the scenarios for the billing, plan the migration of beta users, fix the usage tracking, etc.),
get all feedback from the beta users and prepare them for what will happen next,
plan the work needed for launch and decide on when to do it.
I hope you enjoyed this weekās updates! š
If youāre interested in following my journey, make sure to subscribe or follow me on X/Twitter and LinkedIn!
Appendix
Personal branding
X
X Premium analytics
Beehiiv Analytics
Reply