Building a Document and Contract SaaS with SaasRock — Part 5— Pricing Plans
After 6 weeks of being busy and exhausted, here’s the next part of the series.
Check out part 4 here.
Chapter 5
- Supported Pricing Models
- Plan Feature Limits
- Configuring the Subscription Plans
- Creating the Prices on Stripe
- Subscribing and Cancelling
1. Supported Pricing Models
SaasRock supports the 4 pricing models, read more about them here.
- FLAT_RATE — e.g. $9/month or $90/year
- PER_SEAT — e.g. $9/user/month
- USAGE_BASED — e.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.
- Monthly — e.g. 10 employees/month
- Max — e.g. 100 employees
- Not included — e.g. priority support
- Included — e.g. integrate with Google Sheets
- Unlimited — e.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):
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:
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 “generateCommonPlan” that 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”.
At first try, I don’t like the outcome for three reasons:
- The second plan is taller than the other two because the Description is longer
- The default currency is USD, I want it to be MXN
- The last 2 features are not translated
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.
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”.
This should generate the Products and Prices on Stripe:
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:
Or create more than 1 contract:
Cancelling
If I cancel my plan, it will keep it there util the cycle ends, in this case one month from now, April 2023.
And I can click on “View all plans & prices” to 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):
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:
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!