GenerateSaaS

Plans & Pricing

Configure subscription tiers and per-currency pricing across monthly, yearly, and lifetime billing intervals using pricingConfig.

Plans live in pricingConfig - a standalone named export from packages/config/src/pricing.ts, not a key on config. It holds plans: PricePlan[], a defaultPlan ID, and optional credits/products blocks, and renders the marketing pricing page even when config.payment.enabled is false.

Shipped plans

Three plans ship out of the box; tier order is array position - reorder only to change upgrade/downgrade logic.

PlanidtypeNotable
FreefreefreedefaultPlan - assigned to new users
Starterstartersubscriptionmonth + year
Proprosubscriptionfeatured: true, month + year
  • defaultPlan ships "free" (set null for no default plan).
  • defaultApiRateLimit is the fallback when a plan omits apiRateLimit (ships { maxRequests: 100 }).
  • No plan ships a free trial; add trialDays (and optionally grantCreditsOnTrial) to any paid plan to offer one.

PricePlan fields

Each entry in plans is a PricePlan (packages/config/src/types/index.ts).

FieldTypeNotes
idstringStable code identifier (e.g. "pro"); becomes user.plan
type"subscription" | "one_time" | "free"Billing model
namestringi18n key - resolved at render
descriptionstringi18n key
featuresstring[]i18n keys - bullet list
limitsstring[]i18n keys - usage limits (optional)
pricesPriceOption[]One entry per interval
featuredbooleanHighlight as recommended (only pro by default)
creditsnumberGranted on subscribe - see Credits
creditIntervalnumberDays between credit refresh (e.g. 30)
apiRateLimitApiRateLimitConfig{ maxRequests, timeWindow? } per plan; timeWindow defaults to 24h (86400000 ms)
trialDaysnumberFree trial length
grantCreditsOnTrialbooleanGrant credits during trial (default false)

name, description, features, and limits are i18n keys, not literal copy. Edit packages/i18n/translations/en/web.json - never hardcode text in pricing.ts. See i18n.

PriceOption fields

A plan's prices array carries one PriceOption per billing interval.

FieldTypeNotes
interval"month" | "year" | "lifetime"Billing cycle
amountsRecord<string, number>Major units keyed by currency (e.g. { USD: 29, EUR: 26 })
anchorAmountsRecord<string, number>Optional strike-through "was" prices
stripePriceIdstringProvider price ref (Stripe)
polarProductIdstringProvider price ref (Polar)
featuredbooleanHighlight this option (e.g. "Best Value")

Every currency code used in amounts must exist in config.currency.list, or it can't be selected or displayed.

A plan, annotated

The pro plan shows the full shape - two intervals, anchor pricing, and one provider ref per price. The generated config carries only the provider you picked at init (stripePriceId for Stripe, polarProductId for Polar) and amounts in the single currency you chose:

// packages/config/src/pricing.ts
{
  id: "pro",
  type: "subscription",
  name: "pricing.plans.plan_3.name",         // i18n key
  description: "pricing.plans.plan_3.description",
  featured: true,                            // recommended badge
  features: ["pricing.plans.plan_3.features.item_1", /* … */],
  prices: [
    {
      stripePriceId: "",                     // your chosen provider's ref - fill from the dashboard
      interval: "month",
      amounts: { USD: 29 },
      anchorAmounts: { USD: 39 },            // strike-through
      featured: true,
    },
    { stripePriceId: "", interval: "year", amounts: { USD: 290 }, anchorAmounts: { USD: 349 } },
  ],
  credits: 50,                               // granted on subscribe
  creditInterval: 30,                        // refresh every 30 days
  apiRateLimit: { maxRequests: 5000 },
}

Generated plans ship with empty stripePriceId/polarProductId strings. Create the matching prices in your provider dashboard and paste the IDs back here, or checkout fails.

Currency

Display currency comes from config.currency in packages/config/src/index.ts.

KeyTypeDefaultDescription
basestring"USD"Base currency code for pricing
listCurrency[]USD, EURSelectable currencies (symbol, code, place, space)
countryMap{ default } & Record<string,string>27 EU → EURMaps ISO country code → currency code
  • The mainCurrency export resolves the base currency object from list.
  • countryMap.default is the fallback when a visitor's country isn't mapped.
  • Currency selection + rendering are framework-specific - see Data fetching and Theming.

Frequently asked questions

Why isn't pricingConfig on config? Pricing is a large, frequently-edited data set, so it lives in its own module to keep config/src/index.ts lean. Import it directly: import { pricingConfig } from "@repo/config".

Do plans render when payments are off? Yes. Plan cards always render from pricingConfig; only checkout and the active provider are hidden when config.payment.enabled is false.

How do I add a currency? Add a Currency object to config.currency.list, then add that code to every amounts/anchorAmounts map you want to support. Codes missing from list are unselectable.

What's the difference between a plan and a product? Plans set a recurring entitlement and change user.plan; one-time products grant standalone ownership and never touch user.plan. See One-time products.

On this page