Skip to main content

Plans

Plans define the pricing tiers available on the platform. Each plan sets the limits and features available to accounts subscribed to it — API call limits, scan limits, and feature flags.

Plan structure​

Each plan row in the database contains:

FieldDescription
nameDisplay name (e.g. Free, Pro, Business)
monthly_pricePricing in cents
lemonsqueezy_product_idLemon Squeezy product ID
lemonsqueezy_monthly_variant_idMonthly billing variant ID
stripe_monthly_price_idStripe monthly price ID
Feature limit columnsAPI call limits, scan limits, etc.

The monthly variant/price columns are also how an incoming webhook is matched back to a plan when it carries no plan_id in its metadata, so fill them in for every paid plan you sell.

API endpoints (user-facing)​

MethodEndpointDescription
GET/plansList all available plans
POST/upgradeUpgrade or change the account's plan

API endpoints (master)​

MethodEndpointDescription
GET/master/plansList all plans with account counts
POST/master/plansCreate a new plan
PUT/master/plans/:idUpdate a plan's limits or pricing IDs
DELETE/master/plans/:idDelete a plan

Populating plans​

After running migrations, insert your pricing tiers with the corresponding payment gateway IDs. See Lemon Squeezy Setup for how to link variant IDs.

Plan history​

plan_history is a versioned snapshot of each plan's pricing and limits, not a per-account log. Editing a plan closes the current snapshot (is_current = false, effective_to set) and opens a new one, so accounts already billing against the old snapshot keep the limits they signed up for.

Quota enforcement always reads the snapshot referenced by the account's billing cycle (app_usage.plan_history_id), never the live plans row.

One current snapshot per plan

Exactly one row per plan should have is_current = true. The lookups that resolve a plan's limits take the first match without ordering, so duplicates make quotas non-deterministic. Always close the previous snapshot before opening a new one — planHistory.markAllCurrentAsSuperseded() then planHistory.createFromPlan(), as model/masterPlans.js does.