Skip to main content

SubscriptionInfo

Current subscription information for an organization, including plan details and feature access

type SubscriptionInfo {
planName: String
monthlyDeposit: Float!
status: SubscriptionStatus!
currentPeriodEnd: Time
features: [String!]!
suspended: Boolean!
trialEnd: Time
billingInterval: String
pendingDowngradePlanName: String
hasStripeSubscription: Boolean!
hasPlanAccess: Boolean!
lapsedFromPlanName: String
trialCancelsWithoutPaymentMethod: Boolean!
freeAllowance: FreeBacktestAllowance
}

Fields​

SubscriptionInfo.planName ● String scalar​

SubscriptionInfo.monthlyDeposit ● Float! non-null scalar​

SubscriptionInfo.status ● SubscriptionStatus! non-null enum​

SubscriptionInfo.currentPeriodEnd ● Time scalar​

When the current billing period ends. NULL on the Free plan, which has no billing period at all — render "renews"/"ends" copy only when this is set, never a fabricated date.

SubscriptionInfo.features ● [String!]! non-null scalar​

SubscriptionInfo.suspended ● Boolean! non-null scalar​

SubscriptionInfo.trialEnd ● Time scalar​

SubscriptionInfo.billingInterval ● String scalar​

Stripe billing interval: "month" or "year". Null if no subscription exists.

SubscriptionInfo.pendingDowngradePlanName ● String scalar​

When set, a downgrade is scheduled for the next billing period. The current plan's features remain active until currentPeriodEnd.

SubscriptionInfo.hasStripeSubscription ● Boolean! non-null scalar​

Whether a Stripe subscription actually backs this posture RIGHT NOW.

False on the Free plan, which is what an organization with no stripe_subscriptions row is on. Such an org has no invoice, no billing period and nothing to switch, cancel or manage — its purchase path is createSubscriptionSession, never changeSubscriptionPlan, and offering it a "Cancel subscription" button cancels something that does not exist.

Distinct from "has this org ever transacted with us" (that is a server-side question the trial's safety rests on, deliberately not exposed here).

SubscriptionInfo.hasPlanAccess ● Boolean! non-null scalar​

Whether the plan named by planName is in force RIGHT NOW — the single answer to "may this organization use the product at all".

GATE ON THIS, NEVER ON status. status is the SUBSCRIPTION's status, and the two are deliberately different for a lapsed organization: a canceled or past_due subscription now falls back to the Free plan, so it reports planName "Free", Free's features, hasPlanAccess TRUE and status "canceled" — all four at once, and all four true. A client that allow-lists active/canceling/trialing shows such an organization a paywall while the server is happily serving it the Free plan.

False only when no plan is in force at all, which is a state the server does not currently produce for any organization. It exists so that the decision stays on the server: billing policy is not a thing the console should be re-deriving from an enum.

SubscriptionInfo.lapsedFromPlanName ● String scalar​

The plan this organization LAPSED FROM, when it is on Free because a paid subscription ended rather than because it never had one.

NULL for every organization that is not lapsed — including every paying one and every org that has only ever been on Free. Non-null is what distinguishes "churned onto Free" from "new signup on Free", and it is the cue for honest copy ("your Pro plan ended") instead of a generic upgrade prompt.

SubscriptionInfo.trialCancelsWithoutPaymentMethod ● Boolean! non-null scalar​

True when this organization is on a trial that Stripe will CLOSE at trialEnd because no payment method is on file.

This is the trial's actual outcome, not a preference: the trial is created with trial_settings.end_behavior.missing_payment_method="cancel", so a trial reaching its end with no card does not convert and does not invoice — the SUBSCRIPTION closes. The organization is not locked out: it falls back to the Free plan (see hasPlanAccess and lapsedFromPlanName). Render the "add a payment method" path on this and nothing else.

Distinct from the trialRequiresPaymentMethod QUERY, which is a deployment-wide configuration fact about whether a card is demanded to START a trial. This field is per-subscription and answers a different question: does THIS organization still need to act.

Always false when not trialing — including for every paid subscription, which necessarily has a payment method already.

SubscriptionInfo.freeAllowance ● FreeBacktestAllowance object​

How much of the Free plan's LIFETIME backtest allowance the CALLING USER has left, under this organization's plan.

NULL on every plan that declares no lifetime cap — which is every paid plan. The console must read NULL as "render no counter at all": showing "0 of 0 runs left" to an Enterprise org would be worse than showing nothing.

The subject is always the caller. billing.Principal is built from the validated token (sub + azp) and ResolveFreeAllowance takes no subject argument, so this field cannot be aimed at another user. The organization decides only WHICH PLAN is in force; the counter itself is user-scoped and spans every organization that user has ever belonged to.

Populated EAGERLY by each producer from the ownerID its own @hasScope already authorized — never by a gqlgen field resolver that re-derives one. A field resolver receives no tenant identity (model.SubscriptionInfo carries no ownerID), so it would have to read the parent's args, and those are spelled ownerID on three producers and ownerId on the subscription. A miss there yields "" — which does NOT fail closed, because ResolveEntitlement turns an unresolvable ownerID into ent.IsNotFound and therefore into the FREE entitlement. The bug's shape would be a Free run counter rendered over an organization that is actually paying.

Returned By​

cancelSubscription mutation ● changeSubscriptionPlan mutation ● organizationBillingChanged subscription ● reconcileCheckoutSession mutation ● subscriptionInfo query