SaasRock v0.6 — Usage-based, One-time, and Multi-currency pricing

SaasRock v0.6 — Usage-based, One-time, and Multi-currency pricing

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

August wrap up

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:

New database models

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

FLAT_RATE

PER_SEAT - /pricing?model=1

PER_SEAT

USAGE_BASED - /pricing?model=2

USAGE_BASED

FLAT_RATE_PLUS_USAGE_BASED - /pricing?model=3

FLAT_RATE_PLUS_USAGE_BASED

ONE_TIME - /pricing?model=4

ONE_TIME

Once you're somewhat happy with the plans, devs can easily generate them from /admin/setup/pricing:

/admin/setup/pricing

But you can also create them one by one at /admin/setup/pricing/new:

New pricing plan

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?

Plan Features & Limits

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:

Flat rate + Usage-based

Our Starter product, should look like this, note that I'm supporting USD and MXN currencies.

Starter Plan - Stripe Dashboard

The Stripe Checkout session would look like this:

  • Pay $199 now
  • Pay based on API calls usage at the end of the cycle

Starter Plan - Stripe Checkout Session

The Stripe subscription will be Active, have two Subscription Items, and have an Upcoming Invoice.

Customer Subscription - Stripe Dashboard

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:

GET employees API call - VS Code Thunder Client

The Upcoming invoice will still be $199 because API calls are free until we reach 10 units:

Stripe Upcoming Invoice

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:

$199 + (0.04 x 1) + $10 = $209.44

And our customer can see the Upcoming Invoice at /app/:tenant/settings/subscription:

Upcoming Invoice

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:

$199 + (0.02 x 21) + $20 = $219.42

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.

Upcoming Invoice - Graduated

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, "ENTITYNAME");

The reportUsage(tenant, unit) function handles everything:

reportUsage(tenant, unit)

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

To showcase this, I went and created a new plan called "All-access" at /admin/setup/pricing/new.

One-time plan

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.

3 recurring + 1 one-time plans

Finally, our subscription looks like this:

All-access + Starter plans


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.

app/utils/db/appConfiguration.db.server.ts

/register

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.

Tenant Subscription

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:

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

/pricing

Stripe Checkout

Stripe Checkout

Checkout Success page

/pricing/:session_id/success

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.

Set up Account Email

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

Bought plans already added to a tenant/account

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

getAppConfiguration()


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:

locales/…/translations.json

You can use those keys when creating/updating Plan Features:

Features

So your pricing page would be dynamic (currency and language):

English & Spanish - Pricing page

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:

Starter and Enterprise plans

I should have the right to:

  • 5 + 12 = 17 users
  • 45 + 90 = 135 contracts/month
  • Unlimited API calls!
  • Priority support

Merged features

You could customize this functionality by modifying the mergeFeatures(features) function at the subscriptionService.ts file:

mergeFeatures([])

And what about per seat pricing plans? Let's see if it also works. 3 Starter Seats:

3 Starter Seats

My Subscription

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.

Multiple units

If I subscribe to the Starter plan, this is the Checkout page:

$199 + 2 usage-based prices

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

$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):

SaasRock community feedback

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

Did you find this article valuable?

Support Alexandro Martínez by becoming a sponsor. Any amount is appreciated!