Subscription Plans

Configure the subscription plans available to users.

Configure Subscription Plans

Subscription plans are defined in packages/utils/src/constants/billing.ts:

packages/utils/src/constants/billing.ts
const _plans = [
  // ! DO NOT REMOVE THE FREE PLAN
  // (you can customize it)
  {
    name: "Free", // Can be customized. IMPORTANT: If you change this value, you must also update the default plan in the database schema (schema.prisma, line 27) so it matches.
    lookupKey: "free", // ! DO NOT CHANGE THIS VALUE
    features: ["Basic features", "Community support", "1 project"], // Can be customized
    // The application reacts to the intervals configured in the Free plan
    // Available options:
    // 1. `intervals: false` - No intervals (no different prices for month/year)
    // 2. `intervals: { [BillingInterval.MONTH]: {}, [BillingInterval.YEAR]: {} }` - Different prices for month/year (lookupKey will be automatically generated as `free_month` and `free_year`)
    // 3. `intervals: { [BillingInterval.MONTH]: { lookupKey: "pro_custom_month" }, [BillingInterval.YEAR]: { lookupKey: "pro_custom_year" } }` - Different prices for month/year with custom lookup keys (you can add a custom lookup key only for one interval)
    intervals: {
      [BillingInterval.MONTH]: {},
      [BillingInterval.YEAR]: {},
    },
  },
  // Your plans
  // (you can add/remove/customize them)
  {
    name: "Pro",
    lookupKey: "pro",
    features: ["Everything in Free", "Priority support", "10 projects"],
    intervals: {
      [BillingInterval.MONTH]: {},
      [BillingInterval.YEAR]: {},
    },
  },
  {
    name: "Ultimate",
    lookupKey: "ultimate",
    features: ["Everything in Pro", "Dedicated support", "Unlimited projects"],
    intervals: {
      [BillingInterval.MONTH]: {},
      [BillingInterval.YEAR]: {},
    },
  },
] as const satisfies readonly Plan[];

The Free plan is required and must not be removed.

You can customize its name and features, but if you change the name, update the default plan in the database schema (schema.prisma, line 27) so it matches. Do not change the lookup key of the Free plan.

PropertyDescription
nameDisplay name of the plan.
lookupKeyUsed to match the plan with Stripe products. Lookup keys for intervals are auto-generated (e.g., pro_month, pro_year).
featuresList of features displayed on the pricing page.
intervalsBilling intervals. Set to false for no intervals. You can optionally provide custom lookup keys per interval.

The subscription plans are flexible. You can add, remove, or customize them as you need (e.g., change the name, lookup key, features, or intervals).

The application reacts to the intervals configured in the Free plan.

If you set intervals to false in the Free plan but set { [BillingInterval.MONTH]: {}, [BillingInterval.YEAR]: {} } in any other plan, the application will not have intervals.

Let's look at a few examples.

Example 1: No Intervals

packages/utils/src/constants/billing.ts
const _plans = [
  {
    name: "Free",
    lookupKey: "free",
    features: ["Basic features", "Community support", "1 project"],
    intervals: false,
  },
  {
    name: "Pro",
    lookupKey: "pro",
    features: ["Everything in Free", "Priority support", "10 projects"],
    intervals: false,
  },
  {
    name: "Ultimate",
    lookupKey: "ultimate",
    features: ["Everything in Pro", "Dedicated support", "Unlimited projects"],
    intervals: false,
  },
] as const satisfies readonly Plan[];

In this example, the application will not have intervals.

When pushing the plans to Stripe, the user will be prompted to choose the interval applied to all plans and the price of each plan.

The lookup keys must be unique.

Example 2: Different Prices for Month and Year

packages/utils/src/constants/billing.ts
const _plans = [
  {
    name: "Free",
    lookupKey: "free",
    features: ["Basic features", "Community support", "1 project"],
    intervals: {
      [BillingInterval.MONTH]: {},
      [BillingInterval.YEAR]: {},
    },
  },
  {
    name: "Pro",
    lookupKey: "pro",
    features: ["Everything in Free", "Priority support", "10 projects"],
    intervals: {
      [BillingInterval.MONTH]: {},
      [BillingInterval.YEAR]: {},
    },
  },
  {
    name: "Ultimate",
    lookupKey: "ultimate",
    features: ["Everything in Pro", "Dedicated support", "Unlimited projects"],
    intervals: {
      [BillingInterval.MONTH]: {},
      [BillingInterval.YEAR]: {},
    },
  },
] as const satisfies readonly Plan[];

In this example, the application will have different prices for month and year.

When pushing the plans to Stripe, the user will be prompted to choose the price of each plan for each interval.

The lookup keys for the prices will be generated automatically as pro_month, pro_year, ultimate_month, and ultimate_year. If you set a custom lookup key for a plan, it will generate the lookup keys for the intervals as custom_month and custom_year.

The lookup keys must be unique.

Example 3: Different Prices for Month and Year with Lookup Keys

Not recommended unless you have a specific reason to use custom lookup keys.
packages/utils/src/constants/billing.ts
const _plans = [
  {
    name: "Free",
    lookupKey: "free",
    features: ["Basic features", "Community support", "1 project"],
    intervals: {
      [BillingInterval.MONTH]: {
        lookupKey: "free_custom_month",
      },
      [BillingInterval.YEAR]: {
        lookupKey: "free_custom_year",
      },
    },
  },
  {
    name: "Pro",
    lookupKey: "pro",
    features: ["Everything in Free", "Priority support", "10 projects"],
    intervals: {
      [BillingInterval.MONTH]: {
        lookupKey: "pro_custom_month",
      },
      [BillingInterval.YEAR]: {
        lookupKey: "pro_custom_year",
      },
    },
  },
  {
    name: "Ultimate",
    lookupKey: "ultimate",
    features: ["Everything in Pro", "Dedicated support", "Unlimited projects"],
    intervals: {
      [BillingInterval.MONTH]: {
        lookupKey: "ultimate_custom_month",
      },
      [BillingInterval.YEAR]: {
        lookupKey: "ultimate_custom_year",
      },
    },
  },
] as const satisfies readonly Plan[];

In this example, the application will have different prices for month and year.

When pushing the plans to Stripe, the user will be prompted to choose the price of each plan for each interval.

In this case, the lookup keys for the prices will be pro_custom_month, pro_custom_year, ultimate_custom_month, and ultimate_custom_year. The application will not generate lookup keys.

The lookup keys must be unique.

Example 4: Mixed

Not recommended unless you have a specific reason to use custom lookup keys.
packages/utils/src/constants/billing.ts
const _plans = [
  {
    name: "Free",
    lookupKey: "free",
    features: ["Basic features", "Community support", "1 project"],
    intervals: {
      [BillingInterval.MONTH]: {},
      [BillingInterval.YEAR]: {},
    },
  },
  {
    name: "Pro",
    lookupKey: "pro",
    features: ["Everything in Free", "Priority support", "10 projects"],
    intervals: {
      [BillingInterval.MONTH]: {
        lookupKey: "pro_custom_month",
      },
      [BillingInterval.YEAR]: {},
    },
  },
  {
    name: "Ultimate",
    lookupKey: "ultimate",
    features: ["Everything in Pro", "Dedicated support", "Unlimited projects"],
    intervals: {
      [BillingInterval.MONTH]: {},
      [BillingInterval.YEAR]: {
        lookupKey: "ultimate_custom_year",
      },
    },
  },
] as const satisfies readonly Plan[];

In this example, the application will have different prices for month and year.

When pushing the plans to Stripe, the user will be prompted to choose the price of each plan for each interval.

The lookup keys you configured will be used as you defined them and the ones you did not configure will be generated automatically.

In this case, the lookup keys for the prices will be pro_custom_month, pro_year, ultimate_month, and ultimate_custom_year.

The lookup keys must be unique.

Example 5: Invalid Configuration

Invalid configurations will break the application.
packages/utils/src/constants/billing.ts
const _plans = [
  {
    name: "Free",
    lookupKey: "free",
    features: ["Basic features", "Community support", "1 project"],
    intervals: {
      [BillingInterval.MONTH]: {},
      [BillingInterval.YEAR]: {},
    },
  },
  {
    name: "Pro",
    lookupKey: "pro",
    features: ["Everything in Free", "Priority support", "10 projects"],
    intervals: false,
  },
  {
    name: "Ultimate",
    lookupKey: "ultimate",
    features: ["Everything in Pro", "Dedicated support", "Unlimited projects"],
    intervals: {
      [BillingInterval.MONTH]: {},
      [BillingInterval.YEAR]: {
        lookupKey: "ultimate_custom_year",
      },
    },
  },
] as const satisfies readonly Plan[];

In this example, the configuration is invalid because the application reacts to the intervals configured in the Free plan and the Pro plan is missing the intervals configuration.