Leave Localhost logoLeave LocalhostDocs
Billing

Billing Catalog

The billing catalog defines every plan available in your product. It lives at packages/backend/convex/billing/plans.config.ts.

The billing catalog defines every plan available in your product. It lives at packages/backend/convex/billing/plans.config.ts.

catalog.ts, provider adapters, validators, and webhooks are framework helpers. For normal product changes, edit plans.config.ts first.

Default Catalog

export const billingPlanCatalog = {
  free: {
    planKey: "free",
    displayName: "Free",
    interval: null,
    sortOrder: 0,
    capabilityKeys: [],
  },
  pro_monthly: {
    planKey: "pro_monthly",
    displayName: "Pro",
    interval: "month",
    sortOrder: 1,
    capabilityKeys: [
      "feature.pro",
      "workspace.members.invite",
      "workspace.members.limit.10",
      "billing.portal",
    ],
  },
  pro_yearly: {
    planKey: "pro_yearly",
    displayName: "Pro",
    interval: "year",
    capabilityKeys: [/* same as monthly */],
  },
  pro_lifetime: {
    planKey: "pro_lifetime",
    displayName: "Pro Lifetime",
    interval: "lifetime",
    capabilityKeys: [/* same as monthly */],
  },
};

Adding a New Plan

  1. Add the plan entry to billingPlanCatalog in plans.config.ts:

    enterprise_monthly: {
      planKey: "enterprise_monthly",
      displayName: "Enterprise",
      interval: "month",
      sortOrder: 4,
      capabilityKeys: [
        "feature.pro",
        "feature.enterprise",
        "workspace.members.invite",
        "workspace.members.limit.unlimited",
        "billing.portal",
      ],
    },
  2. Register new capabilities (if any) in permissions/capabilities.ts:

    export const capabilityKeys = [
      // ...existing
      "feature.enterprise",
    ] as const;
  3. Add capability to the validator in permissions/validators.ts so it can be stored in the database.

  4. Map the provider product in providerPlanEnvMappings in plans.config.ts. Add entries for whichever providers you support:

    providerPlanEnvMappings = {
      stripe: {
        // ...existing
        STRIPE_ENTERPRISE_MONTHLY_PRICE_ID: "enterprise_monthly",
      },
      polar: {
        // ...existing
        POLAR_ENTERPRISE_MONTHLY_PRODUCT_ID: "enterprise_monthly",
      },
      lemon_squeezy: {
        // ...existing
        LEMON_SQUEEZY_ENTERPRISE_MONTHLY_VARIANT_ID: "enterprise_monthly",
      },
    } as const;
  5. Add the plan key to billingPlanKeyValidator in plans.config.ts. Convex validators are intentionally explicit so unknown plan keys fail closed.

  6. Set the environment variable with the provider's product/price ID:

    STRIPE_ENTERPRISE_MONTHLY_PRICE_ID=price_xxx
  7. Add permission rules (optional) in permissions/appPermissions.ts if the new plan unlocks new features:

    "feature.enterprise.use": {
      betterAuth: { feature: ["enterprise.use"] },
      capabilities: ["feature.enterprise"],
    },
  8. Expose the plan in pricing UI and checkout calls. Use the internal plan key, not provider IDs:

    await createCheckout({ planKey: "enterprise_monthly" });

    Add UI copy wherever your pricing page lives. Keep the backend permission checks capability-based so Stripe, Polar, and Lemon Squeezy can map to the same internal plan.

Removing a Plan

  1. Remove the entry from billingPlanCatalog in plans.config.ts.
  2. Remove the plan key from billingPlanKeyValidator.
  3. Remove the provider mapping entry from providerPlanEnvMappings.
  4. Remove the environment variable.
  5. Existing grants for the removed plan remain valid until they expire or are revoked. The planKey stored in grants is a string — it does not need to exist in the catalog for grants to function.

Plan Display Order

Plans are sorted by sortOrder in plans.config.ts:

enterprise_monthly: {
  planKey: "enterprise_monthly",
  displayName: "Enterprise",
  interval: "month",
  sortOrder: 4,
  capabilityKeys: ["feature.enterprise"],
}

Update sortOrder when adding or reordering plans.

Next Reads

On this page