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
  customLink: boolean
  communitySupport: boolean
  bulkLinkShortening: boolean
  "created-links": number
  clicks: number
}

type Entitlements = Record<SubscriptionPlans, 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,
    customLink: false,
    communitySupport: false,
    bulkLinkShortening: false,

    // limit/usage entitlement
    "created-links": 100,
    clicks: 100,
  },
  PRO: {
    // Feature gate entitlement
    analytics: true,
    customLink: true,
    communitySupport: true,
    bulkLinkShortening: false,

    // limit/usage entitlement
    "created-links": 1000,
    clicks: 40000,
  },
  PREMIUM: {
    // Feature gate entitlement
    analytics: true,
    customLink: true,
    communitySupport: true,
    bulkLinkShortening: true,

    // limit/usage entitlement
    "created-links": 5000,
    clicks: 150000,
  },
} 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 canAccessFeature and isUsageWithinLimit 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 route handler to control access to features and usage limits.

Checking Feature Access

The canAccessFeature 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 return the analytics data.
  • If the user doesn't have access to the analytics feature, we throw an error.
route.ts
import { withAuth } from "@package/auth"
import { canAccessFeature, CustomError, sendError } from "@package/utils"
import { NextResponse } from "next/server"

export const GET = withAuth(({ user }) => {
  try {
    const hasAccess = canAccessFeature({
      stripeData: user.stripe,
      featureId: "analytics",
    })

    if (!hasAccess) {
      throw new CustomError(
        "Feature not available on your subscription plan. Please upgrade to access this feature.",
        403
      )
    }

    const exampleAnalytics = "This is an example of analytics data"

    return NextResponse.json(exampleAnalytics, { status: 200 })
  } catch (error) {
    return sendError(error, "Failed to get analytics.", 500)
  }
})

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

  • stripeData: The user's stripe data, which contains the subscription plan.
  • featureId: The ID of the feature that you want to check access for.

Checking Usage Limits

The isUsageWithinLimit 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 created-links action.

  • If the user has reached the limit for the created-links action, we throw an error.
  • If the user hasn't reached the limit for the created-links action, we allow the user to create a new link.
route.ts
import { withAuth } from "@package/auth"
import { db } from "@package/db"
import { CustomError, isUsageWithinLimit, sendError } from "@package/utils"
import { NextResponse } from "next/server"

export const POST = withAuth(async ({ user }) => {
  try {
    // Get the number of created links by the user
    const numberOfCreatedLinks = await db.links.count({
      where: {
        userId: user.id,
      },
    })

    // Check if the user has hit the limit of created links
    const hasAccess = isUsageWithinLimit({
      stripeData: user.stripe,
      featureId: "created-links",
      newUsage: 1,
      currentUsage: numberOfCreatedLinks,
    })

    if (!hasAccess) {
      throw new CustomError("You have reached the limit of created links.", 403)
    }

    // Logic for creating a new link

    return NextResponse.json(null, { status: 201 })
  } catch (error) {
    return sendError(error, "Failed to create link.", 500)
  }
})

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

  • stripeData: The user's stripe data, which contains the subscription plan.
  • featureId: The ID of the feature for which you want to check the usage limit.
  • newUsage: How much the usage will increase by.
  • currentUsage: The current number of times the user has performed the action.

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