Subscription Plans

Learn how to configure and push subscription plans to Stripe.

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.

Push Subscription Plans

After configuring your subscription plans, you can push them to Stripe:

pnpm payments:push

This interactive command creates corresponding products and prices in your Stripe account using your configuration in packages/utils/src/constants/billing.ts.

It will push the plans to Stripe (name, lookup key, features) and you will be prompted to specify the price of each plan for each interval.

The price value is in cents (e.g., €10 is 1000).

With this approach, you don't need to manually create products and prices in Stripe.

You can add images and descriptions to the products and prices by going to the Stripe dashboard and editing the pushed products and prices.

Test the integration

Start the development server

Start the development servers:

pnpm dev

Start the Stripe webhook listener in a separate terminal:

pnpm stripe

Subscribe to a plan

Go to http://localhost:3000, sign up, and choose a plan.

Use the test card 4242 4242 4242 4242 with any future expiration date and any CVC to test payments.

Verify in Stripe

Check the Stripe dashboard BillingSubscriptions to see the subscription.