The first release of SaasRock came with only 2 pricing models: Flat-rate and Per-seat. Soon enough I would realize 2 things: I need to support more pricing models, and that pricing is VERY complicated.
This is the biggest SaasRock update since v0.3.
I could not have built this in 2 weeks without the SaasRock pillars:
- Remix: Loaders + Actions = Quick full stack dev & fast app
- Stripe: Billion-dollar payment API provider
- Prisma: Next-generation object–relational mapper
- Tailwind CSS: Fast prototyping and quality designs
Get SaasRock now to lock-in the current price before it increases!
1/3 - The challenge
Look at some examples of what SaasRock did not support:
- Usage-based - 0.01 x ENTITY_NAME_HERE
- Flat-rate + Usage-based - $9/m + 0.01 x API call
- One-time - $499 Lifetime deal or $2,000 implementation fee
Those are just pricing models, I wanted to improve the overall pricing and subscription UX:
- Multi-currency
- Allow for multiple Usage-based units, e.g. 0.01 x API call or $2 x Contract
- Create an account from the
/pricing
page with Stripe Checkout Session - Multiple subscriptions, and handle each billing cycle separately
- One-time payments create lifetime subscriptions
- Enforce tenants (optionally) to have an active subscription
As a good developer, I promised to have this ready by the end of August, and we're almost halfway through September (but I guess this does not only apply to software development).
I'm not going to lie, this was way more complicated than I anticipated, and I'm aware that even with these new features, I'm just scratching the surface in terms of pricing. No wonder why Stripe's worth is in the billions.
2/3 - The solution
The main problem was allowing for multiple subscriptions because the previous model was designed to support one, just take a look at the models before/after diff:
These new models look scary, but gave me a lot of flexibility to handle:
- Multiple recurring subscriptions
- Multiple one-time payments
- Track usage-based reported records (more on this later)
- Allow for plans/products to have multiple prices, e.g. Flat rate of $9 and Usage-based of $0.01 x Employee
Developer Experience first
The development stage when building SaaS apps is a top priority for SaasRock, so creating, updating, and deleting pricing plans to test all scenarios should be as simple as possible.
This is the reason I built a plans.server.ts
file with default Pricing Plans (Basic, Starter, and Enterprise):
- FLAT_RATE
- PER_SEAT
- USAGE_BASED
- FLAT_RATE_PLUS_USAGE_BASED
- ONE_TIME
And before creating them, you can preview each pricing model that would fit your SaaS at the /pricing
page with a ?model=
parameter.
FLAT_RATE - /pricing?model=0
PER_SEAT - /pricing?model=1
USAGE_BASED - /pricing?model=2
FLAT_RATE_PLUS_USAGE_BASED - /pricing?model=3
ONE_TIME - /pricing?model=4
Once you're somewhat happy with the plans, devs can easily generate them from /admin/setup/pricing
:
But you can also create them one by one at /admin/setup/pricing/new
:
My recommended way of doing it though, is using the plans.server.ts
file, this way you would also have to think about your core features and their limits: are API calls monthly? unlimited? not included?
Once the plan is created, there's no way to update the price(s). This is because that's how Stripe works, and it makes sense since updating the price when there are subscribed customers would make an invoicing mess.
Usage-based pricing
Charging users based on their usage requires a separate blog post on its own, but I'll try to talk about the main things.
If you're a developer, I don't have to tell you why Pay-as-you-go gives a lot of flexibility when building software, and that it's a trend.
Let's test with the following pricing plans, which include Flat rate + Usage-based prices:
Our Starter product, should look like this, note that I'm supporting USD and MXN currencies.
The Stripe Checkout session would look like this:
- Pay $199 now
- Pay based on API calls usage at the end of the cycle
The Stripe subscription will be Active, have two Subscription Items, and have an Upcoming Invoice.
Now the fun part, let's report some usage. Since my plan will track API calls, you could follow this guide to learn how to use the API.
The first API call would give us the following response:
The Upcoming invoice will still be $199 because API calls are free until we reach 10 units:
After reaching 11 units (API calls), we'll reach the 2nd tier, where each unit costs $0.04 and there's a flat fee of $10:
And our customer can see the Upcoming Invoice at /app/:tenant/settings/subscription
:
Now let's get to the 3rd tier, with 21 units (API calls), where units cost $0.02 (cheaper) but there's a flat fee of $20:
These examples were with VOLUME tiers, I'm going to do the following steps to try GRADUATED tiers (read more about this here):
- Cancel our subscription (from
/app/:tenant/settings/subscription
) - Run on the database: DELETE FROM "TenantSubscriptionProduct"
- Run on the database: DELETE FROM "SubscriptionProduct"
- Replace "volume", "tiered" with "graduated", "tiered" on
plans.server.ts
- Log out, log in as my admin user and go to
/admin/setup/pricing
- Select Flat rate + Usage-based and generated the plans
- Log out, go to
/pricing
and subscribe to the Starter plan - Created an API key
After reaching 21 units (API calls), the Upcoming Invoice total calculation is different. As we're using GRADUATED prices, each tier accumulates.
You may wonder how I'm reporting usage, it's really simple:
- For API calls: await reportUsage(apiKey.tenantId, "api");
- For custom entities: await reportUsage(tenantId, "ENTITY_NAME");
The reportUsage(tenant, unit) function handles everything:
One-time prices
Finally, we could use this model to charge for:
- Add-ons
- Any digital good: ebooks, courses, videos…
- Implementation services
- Lifetime access
And of course, once bought, it belongs to the customer forever.
To showcase this, I went and created a new plan called "All-access" at /admin/setup/pricing/new
.
Using the same account that I used to test the usage-based pricing (I used the /admin/users
impersonate button), I'll go to the /subscription/:tenant
page to buy the All-access plan.
Finally, our subscription looks like this:
3/3 - Two different paths for subscribing
1) Sign up, then Subscribe
Almost every SaaS requires you to sign up before going to a /subscription page. For me, this is good UX.
But there's one problem, SPAM. You can solve using one or multiple strategies:
- reCAPTCHA
- Email verification
- CSRF fields
SaasRock already supports the three of them.
After our users create their accounts, they can use our app (with its limits), and they can manage their subscription at /app/:tenant/settings/subscription
and click on View all plans & prices to be redirected to /subscribe/:tenant
page.
But what if we could reduce SPAM even more?
2) Subscribe, then Sign up
Asking users to subscribe before creating an account will turn off a lot of prospects for sure, but hey, it will also turn off a lot of bots almost. For me, this is great UX.
This is actually what TailwindUI does: Provide your email and payment details first and set up your account after.
SaasRock uses Stripe Checkout for subscriptions, so 2 main issues arise from this:
- What if the anon user paid for, say the Starter $99 plan, but closed the explorer after that? And even worse, he/she was on incognito mode so the redirected URL with the session ID is lost.
- What if the user used an email that is already registered on the platform? Shouldn't he/she should've used the
/subscribe/:tenant
route instead? Or is he/she trying to create a separate account?
First things first, our user paid from our /pricing
page, so it makes sense that the success redirection URL should be /pricing/session_id/success
.
Pricing page
Stripe Checkout
Checkout Success page
Upon arrival to the Checkout Success page, we need to store the session with a pending=true state in the database, AND send an email to the user with the redirected URL: /pricing/session_id/success
.
This way, we've backed up the session with the subscribed plan(s) by storing it in the database, and by sending it to the user.
And of course, the session cannot be used more than once (only if our session status is pending=false):
If you want to enable/disable one sign-up flow, just update the following flags:
-
required: true → redirects user to
/subscribe/:tenant
-
allowSubscribeBeforeSignUp: false → buttons at
/pricing
are hidden -
allowSignUpBeforeSubscribe: false →
/register
redirects to/pricing
Bonus - Other challenges
I encountered a few tedious problems, but I solved them so you don't have to.
Internationalization
If you're planning to have customers in England and Spain, better to have English and Spanish translations.
Take a look a the following translation keys:
You can use those keys when creating/updating Plan Features:
So your pricing page would be dynamic (currency and language):
If that's not internationalization, I don't know what is 😜.
Feature Limits
Take a look at the following plans that I have subscribed to:
I should have the right to:
- 5 + 12 = 17 users
- 45 + 90 = 135 contracts/month
- Unlimited API calls!
- Priority support
You could customize this functionality by modifying the mergeFeatures(features) function at the subscriptionService.ts
file:
And what about per seat pricing plans? Let's see if it also works. 3 Starter Seats:
3 times the feature limit values:
- 5 x 3 = 15 users
- 45 x 3 = 135 contracts
- 100 x 3 = 300 API calls
Multiple Usage-based Units
I created the following plans, with 2 usage-based units:
- API calls
- Employees (custom entity)
And a flat fee, so every plan has 3 prices.
If I subscribe to the Starter plan, this is the Checkout page:
You could imagine that if I create 10 employees from the API, that would be 20 units 😂, but I'll do it just for demonstration purposes - I'm using VOLUME tiers.
11 Employees created + 11 successful API calls = $219.88
Conclusion - Hard work, but it was worth it 🤘
v0.6 is fun and all, but I'm already hearing SaasRock subscribers asking for:
- Paddle/PayPal alternative integrations
- Stripe Connect for marketplaces or affiliate commissions
- Trial-ending transactional emails
- And more 😮💨…
I can't promise any feature, but I can tell you that the majority of the community wishes for a 100% stripe implementation, so maybe forget about Paddle for a while (or forever):
When v0.7?
Now that the Pricing & Subscriptions got a huge upgrade, I'll start working on another feature that is getting me excited to build: Affiliate + Referrals:
-
/affiliates
: Become an affiliate -
/affiliate
: My dashboard as an affiliate, my analytics, my payouts, my referrals -
/admin/affiliates
: CRUD affiliates, accept requests, view links -
/admin/affiliates/referrals
: Visitors/subscriptions per link -
/admin/affiliates/commissions
: Commission per sale/affiliate, and Paid status (payments would be manual for MVP of course, but I guess a PayPal integration mid-term or Stripe Connect)
Some notes:
- Referrals would be identified by query params (ref, referral, or via)
- Or by custom routes (website.com/my-affiliate-1)
- Referrals could have a coupon (e.g. -20% for 6 months)
- SaasRock subscribers could apply for the affiliate program so you'd be my guinea pigs
Subscribe to the SaasRock newsletter and stay in the loop!
PS: v0.6 will be released Sunday 11th, 2022 at night GMT-5.
Thanks for reading!
- Alex
Top comments (0)