Hacker News new | past | comments | ask | show | jobs | submit login
How we manage plans and features in our SaaS app (checklyhq.com)
159 points by tnolet on Mar 24, 2019 | hide | past | favorite | 41 comments

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.

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.

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.

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.

Valuable addition! And actually, we check do for account_type. So what a trial actually consists of can change over time, as long as the type stays "trial". I left this out of the post for simplicity, but should probably add it.

All billing dunning etc. is done by Stripe billing at this stage.

Yeah, we have one like this: Client to Account link, Account is of Account_Type (plan) and to Account_Ledger. Each has flags, multiple plans have FLAG_DEMO

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.

> 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.

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.

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

I’ve tried writing my own billing system on top of Stripe and PayPal. And I’ve tried using a Paddle-like service (FastSpring, to be precise.)

The bookkeeping and support for my business was much easier when I used FastSpring. I believe the higher payment processing fee to be worth it.

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!

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.

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.

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

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.

No, they are just helpers to show nicer messages to users. The post goes into exactly the topic of validating this stuff on the backend.

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...

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.

I’m actually working on a post literally talking about this - shoot me an email with anything you’d like me to address: sachin@launchdarkly DOT com

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/), 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.

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.

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).

“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.

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.

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.

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...

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

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.

Assuming you have something like customer_name.myapp.com as your dashboard URL, a once-off wildcard SSL for *.myapp.com should suffice, in case you are needing to grab one for each user?

Yes, you are correct for all the customer.checklyhq.com URLs. This everyone gets because it is part of the general wildcard SSL certificate for *.checklyhq.com.

However, customers want to have https://status.example.com for their status pages. This is why we need to provide custom SSL certificates using Lets Encrypt.

That shouldn't be an issue. Having a wildcard cert for .checklyhq.com shouldn't preclude you from having a separate subdomain cert for blog.checklyhq.com etc.

In fact, we do this for our own SaaS and never had a problem. Our AWS instance is protected by *.mydomain.com SSL cert but we have other servers running on digital ocean etc. that have their own subdomains and their own SSL certs.

The tricky thing for us is to filter out the subdomains that we use (e.g. [blog,status,www]), and prevent our customers from using them as their own custom subdomains with our app otherwise our whole DNS redirection doesn't work.

Rich subdomain blacklist should do the trick. Or alternative is: .app.mydomain.com or .mydomainapp.com Eventually require customers to have at least 4 characters names.

Hope customers don’t have cookies scoped to *.example.com

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!

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

I found the same in e-commerce. We manage orders, products, delivery providers, shipments in and out, warehouse stuff, etc.

It’s difficult to design, and there’s very little good information around. Open source code based like Magento are impenetrable with a ton of tech debt, or they are way too simplified for our purposes.

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.

Man you nailed it

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

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.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact