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
| Field | Type | Description |
|---|---|---|
id | string, optional | Rule identifier. Must be nonempty if specified |
description | LocalizedText, optional | Rule description |
trigger | Condition | Conditions that match the user's currently active roles or entitlements |
upgradePlans | OAuthUpgradePlan[] | Plans displayed when the conditions match |
trigger supports the following conditions:
| Form | Meaning |
|---|---|
{ 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
| Field | Type | Description |
|---|---|---|
productId | ProductId | Product ID referencing products.ts |
planId | A plan ID for the corresponding product | Must belong to that product |
title, description | LocalizedText | Plan title and description |
salePrice | string | Display price, such as $9.99 |
listPrice | string, optional | Displayed original price |
discountLabel | string, optional | Discount text |
billingLabel | string, optional | Billing interval text |
recommended | boolean, optional | Recommended flag |
priority | Integer, optional | Display priority |
platforms | readonly string[], optional | Platform 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.