Skip to main content

Overview

The defineAddon() function defines an add-on product that can be purchased on top of a subscription plan. Add-ons can incrementally increase feature limits, set new absolute limits, or grant boolean access to features. When used with a generic type parameter, it ensures that only features defined in your configuration can be referenced.

Function Signature

function defineAddon<
  F extends Record<string, FeatureDefInput> = Record<string, FeatureDefInput>
>(
  config: Omit<AddonDefInput, "features"> & {
    features: F extends Record<string, FeatureDefInput>
      ? Partial<Record<keyof F, AddonFeatureValue>>
      : Record<string, AddonFeatureValue>;
  }
): AddonDefInput

Type Parameters

F
Record<string, FeatureDefInput>
default:"Record<string, FeatureDefInput>"
Feature dictionary type. Pass typeof yourFeatures for strict key validation.

Parameters

config
AddonDefInput
required
The add-on configuration object.

Returns

addon
AddonDefInput
The add-on configuration, normalized to AddonDefInput type.

Usage

Increment Addon (Default Behavior)

Adds to the base plan’s feature limit:
import { defineAddon } from "@revstack/core";

export const extraSeats = defineAddon({
  name: "Extra Seats (5)",
  description: "Add 5 additional team members",
  type: "recurring",
  amount: 500, // $5.00
  currency: "USD",
  billing_interval: "monthly",
  features: {
    seats: {
      value_limit: 5,
      type: "increment", // Adds to the plan's limit
    },
  },
});

// If plan has 10 seats, customer gets 10 + 5 = 15 total seats

Set Addon (Override Behavior)

Completely replaces the base plan’s feature limit:
import { defineAddon } from "@revstack/core";

export const unlimitedStorage = defineAddon({
  name: "Unlimited Storage",
  description: "Remove all storage limits",
  type: "recurring",
  amount: 4900, // $49.00
  currency: "USD",
  billing_interval: "monthly",
  features: {
    storage: {
      value_limit: 999999999999, // Effectively unlimited
      type: "set", // Replaces the plan's limit entirely
    },
  },
});

// Regardless of plan's storage limit, customer gets 999999999999 bytes

Boolean Access Addon

Grants access to a premium feature:
import { defineAddon } from "@revstack/core";

export const ssoAddon = defineAddon({
  name: "SSO Add-on",
  description: "Enable SAML/OAuth Single Sign-On",
  type: "recurring",
  amount: 9900, // $99.00
  currency: "USD",
  billing_interval: "monthly",
  features: {
    sso: {
      has_access: true, // Grants boolean access
    },
  },
});

One-Time Addon

import { defineAddon } from "@revstack/core";

export const onboarding = defineAddon({
  name: "Premium Onboarding",
  description: "White-glove setup and migration service",
  type: "one_time",
  amount: 199900, // $1,999.00
  currency: "USD",
  features: {
    priority_support: {
      has_access: true,
    },
  },
});

Soft Limit Addon

Relaxes a hard limit to allow overage:
import { defineAddon } from "@revstack/core";

export const overageProtection = defineAddon({
  name: "Overage Protection",
  description: "Never get blocked - pay only for what you use",
  type: "recurring",
  amount: 1900, // $19.00
  currency: "USD",
  billing_interval: "monthly",
  features: {
    api_calls: {
      is_hard_limit: false, // Allows overage instead of blocking
    },
  },
});

Multiple Features

import { defineAddon } from "@revstack/core";

export const growthPack = defineAddon({
  name: "Growth Pack",
  description: "Extra seats, storage, and API calls",
  type: "recurring",
  amount: 2900, // $29.00
  currency: "USD",
  billing_interval: "monthly",
  features: {
    seats: {
      value_limit: 10,
      type: "increment",
    },
    storage: {
      value_limit: 50_000_000_000, // 50 GB
      type: "increment",
    },
    api_calls: {
      value_limit: 25000,
      type: "increment",
    },
  },
});

Type-Safe with Feature Dictionary

import { defineConfig, defineFeature, defineAddon } from "@revstack/core";

const features = {
  seats: defineFeature({
    name: "Team Seats",
    type: "static",
    unit_type: "count",
  }),
  storage: defineFeature({
    name: "Storage",
    type: "metered",
    unit_type: "bytes",
  }),
};

export default defineConfig({
  features,
  plans: { /* ... */ },
  addons: {
    extra_seats: defineAddon<typeof features>({
      name: "Extra Seats (5)",
      type: "recurring",
      amount: 500,
      currency: "USD",
      billing_interval: "monthly",
      features: {
        seats: { value_limit: 5, type: "increment" },
        // typo_feature: { value_limit: 10 }, // ❌ Compile error!
      },
    }),
  },
});

Add-on Aggregation Logic

The EntitlementEngine aggregates add-ons with the following rules:
  1. Processing Order: “set” add-ons are processed first, then “increment” add-ons
  2. Increment Behavior: Limits are summed (plan: 5 seats + addon: 3 seats = 8 total)
  3. Set Behavior: Completely replaces the previous limit and tracking source
  4. Soft Limits: If ANY source sets is_hard_limit: false, the entire feature becomes soft-limited
  5. Boolean Access: If ANY source grants has_access: true, access is granted

Validation

The validator ensures:
  • Recurring add-ons have a billing_interval that matches the plan’s interval
  • Add-ons only reference features defined in config.features
  • Add-ons referenced in price.available_addons actually exist

Source

Location: packages/core/src/define.ts:70-80

Build docs developers (and LLMs) love