Skip to main content

Pricing Tiers

In this guide, we'll go over how you can enforce access to features in a SaaS application with multiple pricing tiers using Warrant.

What is Tiered SaaS?

Tiered SaaS is a common pricing model used in modern B2B SaaS products. It allows product owners to segment their users into multiple buckets with access to different product features depending on their pricing tier. For example, you might have a 'Free' tier that gives your users access to basic features and a paid 'Pro' tier that offers additional add-ons for power users.

For this guide, let's assume you've built a SaaS application similar to Figma that allows users to create and manage design assets within their teams. It's a B2B app so users are grouped into 'organizations' that represent their company or team.

Let's say this new app has a variety of features including a collaborative editor, projects and analytics. Access to these features depends on an organization's pricing tier. For example, an organization at the 'Pro' tier has access to projects whereas organizations in the 'Free' tier do not.

The full tier to feature mapping in our application is as follows:

FreeProEnterprise
Collaborative EditorEverything in FreeEverything in Pro
ProjectsAnalytics

In the remainder of this guide, we'll go over how you can use Warrant to create these pricing tiers and enforce user access based on organization and tier within the app.

Prerequisites

This guide exclusively uses the Warrant API and thus assumes you have a Warrant account with API keys. If you don't, please first follow the Quickstart to set up your account.

Creating Object Types

In our application, each user belongs to an organization and each organization is associated with a 'Free', 'Pro' or 'Enterprise' tier that grants them access to specific features. In order to enforce these constraints in our app, we need to be able to answer these queries:

  • Which feature(s) belong to the pro pricing-tier?
  • Which pricing-tier is an organization subscribed to?
  • Which organization does a given user belong to?

These queries are all based on relationships between 3 object types: feature, pricing-tier and organization. So let's first create these object types in Warrant.

Feature

Let's start by creating the feature object type. Features only have a subscriber relation which allows us to grant access to specific features (ex. free tier 'subscriber' to the collaborative editor). A user is a subscriber of a feature either directly or if they're a member of a pricing-tier the feature belongs to.

Create the feature object type as follows:

{
"type": "feature",
"relations": {
"subscriber": {
"inheritIf": "anyOf",
"rules": [
{
"inheritIf": "member",
"ofType": "pricing-tier",
"withRelation": "member"
}
]
}
}
}

Tier

Next, we can create the tier object type. Similar to feature, a tier has one relation, member. We can use this relation to associate which organizations are members of a specific tier (ex. org1 is a 'member' of the Free tier).

Create the tier object type as follows:

{
"type": "pricing-tier",
"relations": {
"member": {}
}
}

Organization

Lastly, we'll create an organization object type. Similar to tier and feature, organization has one relation, member. We can use this relation to express which users are members of an organization (ex. user1 is a 'member' of org1).

Create the organization object type as follows:

{
"type": "organization",
"relations": {
"member": {}
}
}

Creating Warrants

Now that our object types are defined, we need to create some warrants to establish the specific relationships between our tiers, features, organizations and users.

Some of these warrants only need to be created once (ex. feature to tier mappings). Others, like 'user to organization' and 'organization to tier' warrants, need to be integrated into our app's new user and tier management logic.

Setting up Feature Tiers

Our feature to tier warrants can be created and managed directly through the Warrant dashboard since we don't expect our features and pricing to change that often.

However, we can still create them via API as follows:

Free Tier

The 'Free' tier only has access to the collaborative editor feature. We can express this through 1 warrant:

# Free tier has access to the 'collaborative_editor' feature
curl "https://api.warrant.dev/v1/warrants" \
-X POST \
-H "Authorization: ApiKey YOUR_KEY" \
--data-raw \
'{
"objectType": "feature",
"objectId": "collaborative_editor",
"relation": "subscriber",
"subject": {
"objectType": "tier",
"objectId": "free",
"relation": "member"
}
}'

Pro Tier

The 'Pro' tier has access to all 'Free' tier features as well as access to the projects feature. We can express this through 2 warrants:

# Pro tier members are also Free tier members
curl "https://api.warrant.dev/v1/warrants" \
-X POST \
-H "Authorization: ApiKey YOUR_KEY" \
--data-raw \
'{
"objectType": "tier",
"objectId": "free",
"relation": "member",
"subject": {
"objectType": "tier",
"objectId": "pro",
"relation": "member"
}
}'
# Pro tier has access to the 'projects' feature
curl "https://api.warrant.dev/v1/warrants" \
-X POST \
-H "Authorization: ApiKey YOUR_KEY" \
--data-raw \
'{
"objectType": "feature",
"objectId": "projects",
"relation": "subscriber",
"subject": {
"objectType": "tier",
"objectId": "pro",
"relation": "member"
}
}'

Enterprise Tier

The 'Enterprise' tier has access to all Pro and Free tier features as well as access to the analytics feature. We can express this through 3 warrants:

# Enterprise tier members are also Free tier members
curl "https://api.warrant.dev/v1/warrants" \
-X POST \
-H "Authorization: ApiKey YOUR_KEY" \
--data-raw \
'{
"objectType": "tier",
"objectId": "free",
"relation": "member",
"subject": {
"objectType": "tier",
"objectId": "enterprise",
"relation": "member"
}
}'
# Enterprise tier members are also Pro tier members
curl "https://api.warrant.dev/v1/warrants" \
-X POST \
-H "Authorization: ApiKey YOUR_KEY" \
--data-raw \
'{
"objectType": "tier",
"objectId": "pro",
"relation": "member",
"subject": {
"objectType": "tier",
"objectId": "enterprise",
"relation": "member"
}
}'
# Enterprise tier has access to the 'analytics' feature
curl "https://api.warrant.dev/v1/warrants" \
-X POST \
-H "Authorization: ApiKey YOUR_KEY" \
--data-raw \
'{
"objectType": "feature",
"objectId": "analytics",
"relation": "subscriber",
"subject": {
"objectType": "tier",
"objectId": "enterprise",
"relation": "member"
}
}'

Associating Users with Organizations

In order to properly manage feature access for users based on their organization, Warrant needs to know every user's organization. The easiest way to hook this up is by adding the following code to the newUserSignUp() handler in the app:

// 'user' is the newly created user, 'org' is user's intended organization
const newWarrant = await warrantClient.Warrant.Create({
object: {
objectType: "organization",
objectId: org,
},
relation: "member",
subject: {
objectType: "user",
objectId: user,
},
});

Associating Organizations with Feature Tiers

Now that we have our features and tiers defined, and we've associated users with organizations, we're almost done. One of the remaining steps is to ensure that Warrant is aware of every organization's pricing tier. The easiest way to hook this up is by adding the following code to the newSubscription() handler in the app:

// 'org' is the organization and 'tier' is the org's intended tier
const newWarrant = await warrantClient.Warrant.create({
object: {
objectType: "tier",
objectId: tier,
},
relation: "member",
subject: {
objectType: "organization",
objectId: org,
relation: "member",
},
});

Enforcing User Access to Features

Now that our access model is set up, the final step is to start enforcing access checks in our app.

For example, you might want to check if a user with userId user1 is authorized to access the analytics feature. To enforce this check, you'd need to add the following access check code in the app:

const isAuthorized = await warrantClient.Authorization.check({
warrants: [
{
object: {
objectType: "feature",
objectId: "analytics",
},
relation: "subscriber",
subject: {
objectType: "user",
objectId: "user1",
},
},
],
});
if (isAuthorized) {
// Allow access to 'analytics' for this user
} else {
// Fail request
}

Summary

In this guide, we created an access model for a B2B SaaS application that allows us to enforce user access to specific features based on their organization and pricing tier (Free, Pro or Enterprise).

Defining this model in Warrant allows us to separate our access logic from our core business logic and enables us to make changes on the fly without having to change application code.

With Warrant, we can 'upgrade' and 'downgrade' an organization's pricing tier and also add/remove features from pricing tiers instantly without having to change any application code.