Upgrade suggestions

OAuth scope upgrade rules, plan details, and default behavior in config/upgrade.ts.

config/upgrade.ts exports websiteScopeUpgradePolicies, which the server reads through getServerConfig().upgrade. It lets the OAuth authorization page suggest plan upgrades based on specific role or entitlement conditions.

Default configuration

import type { OAuthUpgradeRule } from '@/core/services/oauth2-server/types';
import type { Scope } from './scopes';

export const websiteScopeUpgradePolicies = {
} as const satisfies Partial<Record<Scope, OAuthUpgradeRule[]>>;

The active configuration is an empty object. Comments in the source file are illustrative examples, not enabled products or upgrade rules. Do not use them simply by uncommenting them.

Each object key corresponds to a scope in config/scopes.ts and maps to an array of rules. You can omit rules for a scope. If you configure them, the rule array must be nonempty, and each rule's upgradePlans must contain at least one entry.

Rule fields

FieldTypeDescription
idstring, optionalRule identifier. Must be nonempty if specified
descriptionLocalizedText, optionalRule description
triggerConditionConditions that match the user's currently active roles or entitlements
upgradePlansOAuthUpgradePlan[]Plans displayed when the conditions match

trigger supports the following conditions:

FormMeaning
{ type: 'role', operator: 'in', values: [...] }The user has any of the listed roles
{ type: 'role', operator: 'not_in', values: [...] }The user has none of the listed roles
{ type: 'entitlement', operator: 'has', values: [...] }The user has any of the listed entitlements
{ type: 'entitlement', operator: 'not_has', values: [...] }The user has none of the listed entitlements
{ anyOf: [...] }At least one child condition is true
{ allOf: [...] }All child conditions are true
{ noneOf: [...] }All child conditions are false

Condition arrays must be nonempty. Conditions specify when to suggest an upgrade. For example, use not_has to show upgrade suggestions to users who do not yet have an entitlement.

Plan fields

FieldTypeDescription
productIdProductIdProduct ID referencing products.ts
planIdA plan ID for the corresponding productMust belong to that product
title, descriptionLocalizedTextPlan title and description
salePricestringDisplay price, such as $9.99
listPricestring, optionalDisplayed original price
discountLabelstring, optionalDiscount text
billingLabelstring, optionalBilling interval text
recommendedboolean, optionalRecommended flag
priorityInteger, optionalDisplay priority
platformsreadonly string[], optionalPlatform labels

Price strings control only the display. They do not create or modify Stripe Prices or change the amount charged through Checkout. Check the product, plan, payment price, and display text together.

How the authorization page uses these rules

The authorization page evaluates each requested scope. A scope without upgrade rules goes into the list of scopes available for authorization. If a rule matches and returns plans, the scope goes into the list of scopes that require an upgrade, and the plan details are displayed. If no plans match, the scope can still be available for authorization.

When multiple rules for the same scope match the same product and plan, the matching function deduplicates by product and plan, keeping the first plan details encountered.

This configuration provides upgrade guidance on the authorization page. It does not automatically grant roles or entitlements. Application APIs must still check sign-in status, scopes, and application permissions. The payment flow handles entitlements after purchase.

After making changes, run npm run typecheck and check the authorization page with accounts that meet the conditions and accounts that do not. When renaming or deleting scopes, roles, entitlements, products, or plans, update their references here as well.