Building a Document and Contract SaaS with SaasRock — Part 5— Pricing Plans

Alexandro Martinez
6 min readMar 21, 2023

--

After 6 weeks of being busy and exhausted, here’s the next part of the series.

Check out part 4 here.

Chapter 5

  1. Supported Pricing Models
  2. Plan Feature Limits
  3. Configuring the Subscription Plans
  4. Creating the Prices on Stripe
  5. Subscribing and Cancelling
Pricing Plans

1. Supported Pricing Models

SaasRock supports the 4 pricing models, read more about them here.

  • FLAT_RATEe.g. $9/month or $90/year
  • PER_SEATe.g. $9/user/month
  • USAGE_BASEDe.g. From 101 to 200 invoices → $0.01/invoice and a $5 fee
  • FLAT_RATE_PLUS_USAGE_BASED $9/month + usage
  • ONE_TIME$399 once

SaasRock pre-configures these models in the “plans.server.ts” file:

...
const FLAT_RATE_PRICES: SubscriptionProductDto[] = [...];
const PER_SEAT_PRICES: SubscriptionProductDto[] = [...];
const USAGE_BASED_PRICES: SubscriptionProductDto[] = [...];
const FLAT_RATE_PLUS_USAGE_BASED_PRICES: SubscriptionProductDto[] = [...];
const ONE_TIME_PRICES: SubscriptionProductDto[] = [...];

const plans = [
...FLAT_RATE_PRICES,
...PER_SEAT_PRICES,
...USAGE_BASED_PRICES,
...FLAT_RATE_PLUS_USAGE_BASED_PRICES,
...ONE_TIME_PRICES
];

To preview these prices, go to the /pricing route. By default, it will display the “FLAT RATE” prices, if you don't want it like this, change the following line in the “PricingBlockService.server.ts” file which handles the loader and action functions:

...
export namespace PricingBlockService {
export async function load({ request, t }: PageBlockLoaderArgs) {
...
const debugModel: PricingModel =
debugPricingModel ? (Number(debugPricingModel) as PricingModel)
:
+ PricingModel.FLAT_RATE;

return {
items: items.length > 0 ? items : plans.filter((f) => f.model === debugModel || debugModel === PricingModel.ALL),
coupon,
};
}
...

If you run the application, it will display the following Basic, Starter, and Enterprise plans:

These plans are not yet created in Stripe, so they are read-only, if you hover over the “Subscribe” button, you’ll see it’s disabled.

2. Plan Feature Limits

Every plan needs to configure your features, and each one of them has a limit according to its tier. For example, you would want the “Basic” plan to include 10 invoices/month but 1,000/month on the “Enterprise” plan.

  • Monthlye.g. 10 employees/month
  • Maxe.g. 100 employees
  • Not includede.g. priority support
  • Includede.g. integrate with Google Sheets
  • Unlimitede.g. unlimited employees

Depending on how the feature is called, e.g. “users”, I can use the following function to get the current tenant usage:

const usersUsage = await getPlanFeatureUsage(tenantId, "users");
const apiUsage = await getPlanFeatureUsage(tenantId, "api");
const prioritySupport = await getPlanFeatureUsage(tenantId, "priority-support");
const customFeature = await getPlanFeatureUsage(tenantId, "custom-feature");

3. Configuring the Subscription Plans

For this application, I’ll implement the current features, see the following image (prices in MXN):

Délega Pricing Plans

Let’s break them down:

  • Links — Number of linked accounts (Client/Provider relationship)
  • Contracts — Monthly allowed contracts
  • Users — Number of account members
  • Taxpayers (RFC) — Maximum number of companies
  • Storage — GB of storage for documents/contracts

The only one that it’s read-only is “Storage”, all the other ones are functional. You could implement your own logic to measure the storage used per account; since I use Supabase, I would check their API to see if I can keep track of the used storage to limit my users.

Also, the plan names are Free, Standard and Business:

  • Free — $0, 1 link, 1 contract/month, 2 users, and 1 RFC
  • Standard — $99, 45 links, 45/contract/month, 5 users, and 2 RFCs
  • Business — $199, 100 links, 90 contracts/month, 12 users, and 5 RFCs

I have the option to create the plans one by one at “/admin/pricing/new”, but it’s better if I hardcode my plans and features to iterate quickly.

Translations

Since I’m going to support multiple languages, English and Spanish, I’ll configure the plan names and descriptions in the “translations.json” files:

translations.json

Prices

I’m going to use the Flat-rate pricing model, so I’ll update the prices accordingly at “plans.server.ts” in the FLAT_RATE_PRICES array:

Features

There’s a function in plans.server.ts called “generateCommonPlanthat configures these 3 plans, each one of them with its own Title & Description (which can be translation keys) and Features.

IMPORTANT: If you’re using the Entity Builder, the feature name needs to be the same as the Entity name, e.g. “contract”.

Plan Features

At first try, I don’t like the outcome for three reasons:

  1. The second plan is taller than the other two because the Description is longer
  2. The default currency is USD, I want it to be MXN
  3. The last 2 features are not translated
Plans at first try

To fix number 2, I’ll set the MXN currency as the default one at the “app/application/pricing/currencies.ts” file:

...
const currencies: CurrencyDto[] = [
{
name: "United States Dollar",
value: "usd",
symbol: "$",
- default: true,
+ default: false,
disabled: false,
parities: [{ from: "usd", parity: 1 }],
},
...
{
name: "Mexican Peso",
value: "mxn",
symbol: "$",
+ default: true,
disabled: false,
parities: [{ from: "usd", parity: 20.03 }],
},
...

As for the translations, I need to create the following keys for “taxpayers” and “storage” feature names in the “translation.json” files:

"pricing": {
...
"features": {
...
"links": {
"one": "1 linked account",
"max": "{{0}} linked accounts",
"monthly": "{{0}} linked accounts/month",
"unlimited": "Unlimited linked accounts"
},
+ "taxpayers": {
+ "one": "1 taxpayer",
+ "max": "{{0}} taxpayers",
+ "monthly": "{{0}} taxpayers/month",
+ "unlimited": "Unlimited taxpayers"
+ },
+ "storage": {
+ "one": "1 GB of storage",
+ "max": "{{0}} GB of storage",
+ "monthly": "{{0}} GB/month of storage",
+ "unlimited": "Unlimited GB of storage"
+ }
}
...

End result

I’m happy with the result. Although I’ve heard that complicated plans are bad design, maybe I shouldn’t track links, contracts, users, taxpayers and storage, maybe I just need to track links and/or contracts ¯\_(ツ)_/¯, let me know what you think.

Final final for good Pricing Plans (3)

4. Creating the Prices on Stripe

After setting up the STRIPE_SK environment variable (and restarting the app), I can now go to http://localhost:3000/admin/settings/pricing and click “Click here to generate plans”.

Pricing Plans Preview — Admin portal

This should generate the Products and Prices on Stripe:

Stripe Dashboard

5. Subscribing and Cancelling

I’m going to use an incognito tab and subscribe to the free plan.

Current Subscription

At “/app/my-tenant-slug/settings/subscription” I can mange the subscription:

Feature Limits

I should not be able to link with more than 1 account:

Linked Accounts Limits

Or create more than 1 contract:

Contracts Limits

Cancelling

If I cancel my plan, it will keep it there util the cycle ends, in this case one month from now, April 2023.

Cancelled Plan

And I can click on “View all plans & prices” to upgrade:

Upgrade

Currently, SaasRock does not support Prorated Upgrades, meaning that your users would need to cancel their current plan to subscribe to a higher one. And since SaasRock supports multiple plans, I would have my recently cancelled plan (Free) and the upgraded one (Standard):

My Subscription Plans

End Result

If you’re a SaasRock Enterprise subscriber, you can download this code in this release: github.com/AlexandroMtzG/saasrock-delega/releases/tag/part-5-default-pricing-plans.

And here’s the demo:

Demo

What’s next?

In chapter 6, I’ll start working on the Landing page and Branding:

  • Logo
  • Hero Copy
  • Gallery Images
  • Features List
  • SEO

And more marketing-related stuff.

Follow me & SaasRock or subscribe to my newsletter to stay tuned!

--

--

Alexandro Martinez
Alexandro Martinez

Written by Alexandro Martinez

Building SaasRock, The One-Man SaaS Framework built with Remix + Tailwind CSS

No responses yet