
How we manage plans and features in our SaaS app - tnolet
https://blog.checklyhq.com/how-we-manage-plans-features-in-our-saas-app/
======
shimms
From experience, associating an account directly to their current plan in this
was adds some complexity in being able to have a history of plan changes
(downgrades/upgrades etc). You might be accommodating for this in other ways,
but a typical setup I've seen is to add a "subscriptions" table between
account and plan.

The account has many subscriptions, only one of which can be the current. The
subscription is linked to a plan.

When a customer upgrades, the current subscription ends, and is replaced by a
new one. Tracking start and end dates helps with churn and upgrade/downgrade
metrics reports, such as time on premium before downgrade etc.

I've also been stung before by checking for a specific plan, as opposed to
checking for the abilities or attributes of the current plan. In your example
you're checking if the plan name is "trial" to determine if they should have
trial limitations put upon them. This becomes tricky if you need to introduce
a new trial option, named differently in the future, that needs to co-exist
with your current single trial plan set up. I'd prefer to be checking "is the
current plan a trial" or ideally link them to a real plan, but check if it is
"in trial" (via dates, boolean etc).

I've seen (but not done myself) products that let you move up and down between
plans during trial so you can really assess the impact of not having feature
available on higher plans. Doing this is impossible with a single plan called
"trial".

Subscription billing, dunning, pro-rated upgrade, taxes, discounts etc is a
ridiculously complex set of functionality.

~~~
jv22222
Just a note that if you're using Stripe you get that upgrade downgrade history
for free, so might not need the complexity.

Also ProfitWell plugs in to Stripe and does a pretty good job with analytics
for that for free too.

Of course if you're using multiple payment providers you'll want to roll your
own abstraction as the above comment describes.

~~~
nfm
The data isn't lost, but it's not straightforward to query if you don't have a
record of it in your own database.

Eventually you'll want to find out what your resubscribe rate is, or what the
average interval between subscriptions is. Answering these types of questions
via Stripe's API is no fun.

~~~
shimms
Yup - we’ve always duplicated that data in our systems. Aside from anything,
if Stripe/payment-processor-of-choice changes their data model, or rearranges
feature tiers in the future to make reporting beyond the last 180 days an
enterprise add on or whatever, we’d rather not be beholden to them to be able
to analyse our business performance metrics.

------
james_s_tayler
Ok, now explain how to handle the situation where a user that has been
grandfathered into a new set of features and kept paying the original price is
at a tier limit and they downgrade halfway through the period they have
currently already paid for.

Downgrading would put them over a tier limit, so they can't be automatically
downgraded unless they first delete enough stuff to be within the next tier
limit down. Then I suppose they need to get a credit on their account or a
refund etc. But do they get new pricing or are they still grandfathered or
what?

Not to mention if they are a South African customer their invoice needs to
display the VAT subtotal in Rand and Stripe etc don't cater to that. That's on
you.

This stuff becomes kinda nasty when the edge cases stack up and there isn't
anything in the market that handles it all beautifully.

That'd make all our lives a lot easier. SaaS billing still sucks.

It's good to see some discussion on how to handle some of this stuff.
Information is really thin on the ground.

~~~
ai_ia
> Not to mention if they are a South African customer their invoice needs to
> display the VAT subtotal in Rand and Stripe etc don't cater to that. That's
> on you.

I might be wrong but paddle.com helps you solve exactly that.

~~~
aytekin
5% + 50c!

They only make sense if you are small in scale. Use them when you are starting
out and then build your own after you have a good number of paid users.

~~~
imhoguy
Everything has its price. I preffer to improve a core product than to get
drowned in international accounting maze (hello EU VATMESS).

------
j45
This set of blog posts highlights the example of where clever architecture can
outperform clever coding:

\- By abstracting some of the most volitile aspects of an early app and
business logic.. The combinations in which you sell it can greatly increase
the speed of iterating through ideas and offerings.

\- There is more than one valid way to implement feature flagging. It's good
to start somewhere that seems familiar and grow with it.

\- One way I like approaching this problem is tying feature flags into sub
roles and roles, which tie into subscriptions of plans. Depending on the
nature of your app, the previous sentence can be longer, or shorter to allow
flexibility in abstraction.

I'd love to hear how others have seen this implemented in their worlds as
well!

------
conroy
I run a small SaaS app that also has volume-based features. Instead of using
queries to check the count, we compute the total count of resources via
database triggers. I’m not saying this is a better approach, just that there
are multiple ways of doing these calculations.

~~~
tnolet
We actually do this on the frontend with Vue.js computed properties. Triggers
will certainly do that in the backend, but they are bit more initial work than
just a simple query.

~~~
vbsteven
Please tell me you are not basing any plan/usage decision on computed
properties in the frontend without verifying them on the backend

~~~
a13n
Honestly, at an early stage startup, especially if your target audience isn't
developers, it's pretty safe to only do checks on the front end. Just add them
later, once you have enough volume that it's a problem.

------
augustocallejas
I've previously wondered [1] if there was a market for a pricing SaaS, that
does this customer feature tracking separately, like LaunchDarkly, but tied to
payment processing, like Stripe.

[1]: [https://www.indiehackers.com/forum/ask-ih-how-do-you-keep-
tr...](https://www.indiehackers.com/forum/ask-ih-how-do-you-keep-track-of-
your-pricing-plans-in-your-saas-51c8ac5730)

~~~
tnolet
Crazy right. There are very little blogs or docs written on this.

I've kinda struggled with this while signing up the first 20+ customers (and
still am). You sorta figure it out, but it is all custom code tying into
frontend and backend routines. Really does prohibit experimentation.

The blog is a way of getting knowledge out there.

------
lettergram
I have multiple SaaS apps I manage and others which I have assisted
developing.

Starting off, I've always done the following:

1\. Free option with a limited number of queries

2\. Premium option with unlimited number of queries and a few "key" features
unlocked.

This simplifies development significantly, as all you have to do is track the
queries (per hour, day, month, total, etc.). You can also limit routes based
on the features.

This has been extremely effective on my website Easy-A.net
([https://easy-a.net/](https://easy-a.net/)), essentially people can view the
grade distribution for courses and we estimate the workload.

Pricing is always difficult, and by scaling it based on API hits it's much
easier to price. It's essentially pay for use.

I think (as identified in the first line of the article):

> How do you deal with what a user can do on their account in a SaaS app? Can
> Jane on the "Starter" plan create another widget when she is near the limit
> of her plan? What if she's a trial user?

If as a SaaS you're typically targeting one customer archetype starting off --
then you should only have two plans. One to get their feet wet, the other to
help them dive in.

~~~
jv22222
You're probably leaving money on the table with only a free and a paid plan.

Mainly because you'll loose conversions without a high price point third plan
that acts as an anchor and makes the middle plan seem like really good value.

------
anirudh4444
Very well written. We’ve ended up with a lot of similar structures in our
codebase for managing plans and trials. Additionally, we’ve decoupled plans
and trials so that it’s always possible to create a trial of any tier on the
fly; some additional flexibility there. We also chose to do columns in
postgres for features, and not use an array or JSON object for storing it (at
least for now).

------
sessy
“All plans include all features.”

We arrived at this after many years building multiple products. In all
products we built we have just 3-4 plans. All features are included in all
plans (including the trial plan). Only the usage amount differs from plan to
plan. This makes it so easy to explain to end customers and leaves your
engineering team with valuable time to deal with improving product. Further
the number of billing related support requests decrease drastically. Your
support staff also need less training.

The advantages are many to keep billing simple. This has been our experience.
YMMV.

~~~
james_s_tayler
There are definitely a few moves you can make which avoid massive amounts of
complexity if you're willing to accept a small downside. Another one is not
allowing billing cycles to go past the 28th of the month thus making renewal
logic simpler. Another thing would probably be just bill only in one currency
(say USD).

Can you think of any other small things like that that help you massively
reduce complexity? I think in the beginning it seriously makes sense to go
with the simplest billing possible for as long as you can.

------
judge2020
I find this interesting:

> During our 14 day trial we do not give trial users an SSL secured public
> dashboard. Due to technical and abuse reasons this only kicks in when you
> become a paying customer.

I see "technical reasons", but it would make sense to give even trial users a
LetsEncrypt certificate. I don't see how issuing TLS could be abused.

~~~
tnolet
OP here. I guess you caught me there. The SSL feature is actually still in
beta, primed to ship this Monday.

Being fairly new to Lets Encrypt and the eco system around it Checkly is
mostly being cautious. Having a none SSL encrypted dash for the trial is
probably fine.

If this whole process runs super smoothly, we will lift this blockade.

Also, I come from the days where I had to call Verisign in Switzerland and fax
in my passport details...

~~~
ndnxhs
Having no SSL in a trial will probably make customers worry about the products
security and the competence of the devs.

~~~
tnolet
I think you are right about this. The moment I can switch it out to every
other trial user I will probably do it.

Just want to give the Lets Encrypt integration a bit of a trial period before
going 100% in.

------
stevoski
Thanks for writing this.

When I started my SaaS two years ago, I also found it hard to get concrete
info about how to design my database scheme to handle multi-tenancy, plans,
and billing.

In hindsight, it all seems quite straightforward, but I was sometimes quite
lost at the time. Posts like yours would have helped!

~~~
tnolet
This is exactly the reason I'm writing this. The "just add Stripe" mantra is
generally just the beginning of the journey.

------
rkuzsma
Another idea I have heard is to encode your feature flags and entitlements as
a bitwise string, and include this string in your customer's signed auth
token. This way, your front end and back end don't have to hit your
entitlements API all the time. When they change plans, just refresh the auth
token.

------
onemantaker
Man you nailed it

------
bartkappenburg
We just did this for our own SaaS. Our backend is Django and we use
djstripe[0] to connect to Stripe.

The model is as follows: A user pays per site (he can add multiple) and we
decided to have all features available to all plans except for pageviews (we
supply a search service like Algolia, but better ;-), and a notification
service (small persuasive popups) for e-commerce).

We tie a so called 'module' to a site. A module has a product, start date, end
date and subscription (FK). When a user signs up he can start a trial for a
specific product which in effect creates a module with a start date, end date
(now() + trial_length) and an empty subscription (ie. no subscription means
trial).

In our middleware we check if a user wants to view a specific page from a
product and we allow/deny access based on having a valid trial (now <
end_date). We have jobs running that check which trials have ended and we
suspend serving the javascript (which you need to have the service on your
site).

After the trial we redirect to a plan picker (but you can still see reports
for instance) and let them choose a plan. All plan meta data comes from Stripe
as single source of truth (it sits actually in our db because dj-stripe has
cached that using sync and webhooks).

Choosing a plan does the whole CC flow and when we get an OK from Stripe we
attach a subscription (payment tied to a plan) to the module and delete the
end_date. We set the limit in pageviews and check on the usage every minute
using a job. The current usage is also communicated to the customer.

The same check we did with the trial is in place to see if a customer has an
active subscription (now < end_date) and if the current_usage < usage_limit.

Upgrading and downgrading is easy: tie a new plan to the subscription and
Stripe handles the rest (prorate, new invoice).

Canceling is easy as well: cancel call to Stripe, get the returned end_date
and set this on the module.

You also have to think about reactivating the subscription (before the
cancelled subscription has ended).

A fews things to consider:

\- VAT: we're based in the Netherlands, so we have 4 (2x2) cases:
Individual/Company and EU/Non-EU. Based on that we have to charge or charge no
VAT, you have to add that yourself in your call to Stripe.

\- Expired CC, declined payments: Stripe calls a webhook and you have to
handle that in a way that it will (a) change the subscription/module (b) tell
the customer

Our model also allows for:

\- old/defunct pricing plans by setting the subscription to an old plan

\- different/special usage limits for clients using special plans

\- payment without CC (e.g. bank transfer) by using out_of_band option on
Stripe

[0] [https://github.com/dj-stripe/dj-stripe](https://github.com/dj-stripe/dj-
stripe)

~~~
tnolet
Cool stuff! Checkly is also a dutch company, although I live in Berlin.

I will do a write up soon too on how we deal with Stripe and EU taxes. I found
it not THAT hard as people make it out to be.

