GenerateSaaS

Plan grants

Per-plan credit allotments, trial withholding, and the daily recurring re-grant job.

Each plan in pricingConfig.plans may carry these credit fields.

KeyTypeDefaultDescription
creditsnumber0Credits the plan includes per cycle.
creditIntervalnumber (days)0How often credits re-grant. Omit or 0 for a single grant.
grantCreditsOnTrialbooleanunset = withholdGrant credits during a trialDays trial.

The shipped paid plans grant starter: 10, pro: 40, business: 100, all on creditInterval: 30. Two plans grant exactly once, with no interval: free: 5 (a monthly drip against zero revenue is a standing cost a throwaway sign-up per cycle renews forever) and enterprise: 500 (its lifetime price could never fund a perpetual grant). Resolve them with getCreditsForPlan(planId) / getCreditIntervalForPlan(planId), which return 0 when absent.

grantCreditsOnTrial is opt-in and checked as === true: up-front trial credits are farmable across throwaway trials. Left unset, credits are withheld until the first paid grant.

The default plan's free credits are granted once per email address, tracked durably in signup_grant_marks (a SHA-256 of the normalized email). Deleting an account and re-registering the same address assigns the plan but grants no fresh free credits. Inert when the default plan carries no credits.

A subscription's first period is granted at checkout only when Stripe reports its first invoice paid. A SEPA or ACH payment still processing (the subscription already reads active), or one that failed and was voided, sets the plan and grants nothing; the recurring job grants the period once it is paid. Subscribe your Stripe webhook to invoice.paid as well - paid upgrade invoices grant from it.

Recurring re-grants

The recurring-credits job (packages/api/src/functions/billing/recurring-credits.ts) runs daily on cron 0 0 * * *.

Load users on a credit-carrying plan whose payment has cleared. Trialing, past-due, and unpaid subscriptions are excluded, as are banned users and plans that withhold their allotment (planCreditsSuppressed).
Grant when creditsLastGrantedAt + creditInterval has passed and the paid period (planExpiresAt) holds the next slot - at least two intervals past the last grant, less a sixth of an interval of slack - so a period is granted once, never twice (a 31-day month, a leap year, a plan change). Lifetime holdings grant once.
Claim a per-day idempotency key, re-read the account and skip it when its plan changed since the first step (the next run grants on the new plan) or when the plan it now holds withholds its allotment.
Ask the provider whether the current period is paid (ops.isSubscriptionPeriodPaid). Every subscription the account still runs must be paid for its period - a second one still collecting withholds the grant - while one Stripe already reads as ended is skipped. Stripe answers from its own record - the subscription is active, the invoice that opens the current period is paid, and no invoice billing the current period is still owed (an old unpaid invoice for an earlier period does not block it), and that opening invoice was not refunded or credit-noted in full, nor has a disputed charge, a pending refund or a charge that cannot be read (a settled partial refund leaves the period paid; the refund's own clawback takes back its share) - because it reports a renewal active an hour before charging it; any other or unreadable answer waits for tomorrow's run. Then grant via addCreditsAtWatermark(...), which stamps creditsLastGrantedAt in the same update and refuses if a plan change granted the period meanwhile, and write a CREDITS_ADDED audit entry.

A yearly price on a 30-day creditInterval drips twelve allotments per paid year, then waits for the renewal. After a 28- or 29-day month the next grant can land a day or two after the renewal.

Plan changes

Credits follow money, so a plan change only grants what a payment covered.

ChangeCredits
Stripe upgradeThe allotment difference prorated like the price, granted from the paid upgrade invoice (the plan-change event or invoice.paid), once per invoice. Upgrading on the last day of the month buys a thirtieth of the difference. While the period's own grant is still owed (its renewal not yet paid), no difference is granted: the grant on payment delivers the new plan in full.
Polar upgradeNone until the next paid renewal, which grants the new plan in full - Polar may bill the difference on the next invoice.
Interval change (monthly to yearly)The new plan's allotment for the new period, less the unused share of the last grant that the provider credited back, once, restarting the schedule.
DowngradeClawed back to the new plan's allotment; never below it.
Trial or unpaid changeNone until the subscription is paid.
Change paid with account credit or a couponOnly the share paid with new money (amount_due / total) buys credits; a 100%-off switch buys none, and the next paid renewal grants the plan as usual.

Every grant also fires the credits_added notification trigger (gated by config.notifications.enabled), reaching the owner inside an organization since that is whose balance moved. See Background jobs for the scheduler and Audit logs for the entries.

On this page