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
-
Add the plan entry to
billingPlanCataloginplans.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", ], }, -
Register new capabilities (if any) in
permissions/capabilities.ts:export const capabilityKeys = [ // ...existing "feature.enterprise", ] as const; -
Add capability to the validator in
permissions/validators.tsso it can be stored in the database. -
Map the provider product in
providerPlanEnvMappingsinplans.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; -
Add the plan key to
billingPlanKeyValidatorinplans.config.ts. Convex validators are intentionally explicit so unknown plan keys fail closed. -
Set the environment variable with the provider's product/price ID:
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID=price_xxx -
Add permission rules (optional) in
permissions/appPermissions.tsif the new plan unlocks new features:"feature.enterprise.use": { betterAuth: { feature: ["enterprise.use"] }, capabilities: ["feature.enterprise"], }, -
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
- Remove the entry from
billingPlanCataloginplans.config.ts. - Remove the plan key from
billingPlanKeyValidator. - Remove the provider mapping entry from
providerPlanEnvMappings. - Remove the environment variable.
- Existing grants for the removed plan remain valid until they expire or are
revoked. The
planKeystored 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
- Entitlements and Grants — how plans translate to capabilities.
- Free, Pro, and Lifetime Plans — the default plan structure.
Billing Overview
Leave Localhost includes a complete billing system that supports three payment providers out of the box: Polar, Stripe, and Lemon Squeezy. Billing is optional — the app works without any provider configured.
Free, Pro, and Lifetime Plans
Leave Localhost ships with a three-tier plan structure that covers the most common SaaS billing models.