Get NextJet

Entitlements

Entitlements are the features and usage limits that a user has access to based on subscription plan.

I've created a simple and flexible way to manage entitlements based on the subscription plan that a user has.

Types of Entitlements

There are two types of entitlements that you can manage:

  • Features: Features are the functionalities that a user has access to. For example, a user with a PRO subscription plan might have access to advanced features.
  • Usage Limits: Usage limits are the maximum number of times a user can perform an action. For example, a user with a FREE subscription plan might have a limit of 100 requests per day.

Managing Entitlements

There are many different ways of managing entitlements, but I've found that the best way is to use a simple config file to define the entitlements for each subscription plan.

Some developers prefer to use a database to store entitlements, this approach is not recommended as it makes it harder to make changes to the entitlements in case of a change in the subscription plans.

Config File

Inside the packages/utils/src/constants/entitlements.ts file, you can define the entitlements for each subscription plan. Here's an example of how you can define the entitlements:

packages/utils/src/constants/entitlements.ts
type EntitlementFeatures = {
  analytics: boolean // Feature gate
  clicks: number // Usage limit
}

type Entitlements = Record<SubscriptionPlanName, EntitlementFeatures>

// Entitlements are the features and usage limits that a user 
// has access to based on their subscription plan.
export const entitlements: Entitlements = {
  BASIC: {
    // Feature gate entitlement
    analytics: false,
    // Usage entitlement
    clicks: 100,
  },
  PRO: {
    analytics: true,
    clicks: 500,
  },
  PREMIUM: {
    analytics: true,
    clicks: 1000,
  },
} as const

In this example, we have defined the entitlements for the BASIC, PRO, and PREMIUM subscription plans. Each subscription plan has a set of features and usage limits that the user has access to.

Modify this file to define the entitlements for each subscription plan based on your requirements.

Usage

You can use the ensureFeatureAccess and ensureUsageWithinLimit helper functions to check if a user has access to a specific feature or has reached a usage limit based on subscription plan. Do this in a server-side tRPC procedure/api endpoint to control access to features and usage limits.

Checking Feature Access

The ensureFeatureAccess function checks if the user has access to a specific feature based on the subscription plan.

In the below example we are checking if the user has access to the analytics feature.

  • If the user has access to the analytics feature, we allow the user to access the feature.
  • If the user doesn't have access to the analytics feature, we throw an error.

Example usage in a tRPC procedure (packages/api):

export const exampleRouter = createTRPCRouter({
  analytics: protectedProcedure.query(async ({ ctx }) => {
    const subscription = await userRepository.getSubscriptionByUserId(
      ctx.session.user.id
    )

    ensureFeatureAccess({
      featureId: "analytics",
      subscriptionData: subscription,
      notSubscribedErorrMessage:
        "You need to be subscribed to access this feature.",
      noAccessErrorMessage:
        "You need to be subscribed to the PRO plan to access this feature.",
    })

    return "Analytics Data"
  }),
})

The canAccessFeature function takes in an object with the following properties:

  • subscriptionData: The user's subscription data, which contains details about the subscription.
  • featureId: The ID of the feature that you want to check access for.
  • notSubscribedErrorMessage (optional): The custom error message to display if the user is not subscribed.
  • noAccessErrorMessage (optional): The custom error message to display if the user doesn't have access to the feature.

Checking Usage Limits

The ensureUsageWithinLimit function checks if the user has reached the usage limit for a specific action.

In the below example, we are checking if the user has reached the limit for the clicks action.

  • If the user hasn't reached the limit for the clicks action, we allow the user to perform the action.
  • If the user has reached the limit for the clicks action, we throw an error.

Example usage in a tRPC procedure (packages/api):

export const exampleRouter = createTRPCRouter({
  clicks: protectedProcedure.mutation(async ({ ctx }) => {
    const subscription = await userRepository.getSubscriptionByUserId(
      ctx.session.user.id
    )

    // Fetch the current usage count from the database
    const currentNumberOfClicks = await userRepository.getNumberOfClicksByUserId(
      ctx.session.user.id
    )

    // Check if the user has reached the usage limit
    ensureUsageWithinLimit({
      featureId: "clicks",
      usageCount: currentNumberOfClicks,
      usageIncrement: 1,
      subscriptionData: subscription,
      notSubscribedErorrMessage:
        "You need to be subscribed to access this feature.",
      noAccessErrorMessage: 
        "You have reached your usage limit of 50 clicks. Upgrade to increase your limit.",
    })

    // Perform the action and update the usage count in the database
    await userRepository.incrementClicksByUserId(
      ctx.session.user.id
    )
  }),
})

The isUsageWithinLimit function takes in an object with the following properties:

  • subscriptionData: The user's subscription data, which contains details about the subscription.
  • featureId: The ID of the feature for which you want to check the usage limit.
  • usageCount: The current number of times the user has performed the action.
  • usageIncrement: How much the usage count should be incremented by after the performed action. Note: This is not going to actually update the database, it is just for the check.
  • notSubscribedErrorMessage (optional): The custom error message to display if the user is not subscribed.
  • noAccessErrorMessage (optional): The custom error message to display if the user doesn't have access to the feature.

Benefits

The benefits of this approach are that we can easily make changes to the entitlements based on the subscription plan. All we need to do is change one configuration file and the changes will be reflected across the application.

This allows you to easily experiment with different entitlements and subscription plans without major changes to the codebase. It also makes adjustments very simple.

Last updated on