Billing - Migrate from Stripe metering (#20298)

**Overall strategy**
**1. Introduce “Billing V2” behind a workspace flag**
Gate the new model with FeatureFlagKey.IS_BILLING_V2_ENABLED so existing
workspaces stay on the old behavior until they’re migrated or explicitly
on V2.

**2. Replace workflow metered SKUs with a resource-credit product**
Conceptually, billable “workflow execution” usage is not the primary
subscription line item anymore. Add a RESOURCE_CREDIT product (and keep
WORKFLOW_NODE_EXECUTION as deprecated for the transition). Usage and
limits are expressed through credit buckets (e.g. price metadata like
credit_amount), so one product can represent pooled credits instead of a
narrow workflow-only meter.

**3. Migrate subscriptions in two layers**
Schema/catalog: persist extra price metadata (instance upgrade) so the
server knows credit amounts and can match Stripe prices to the new
model.
Per workspace: the registered workspace command
upgrade:2-2:migrate-to-billing-v2 finds subscriptions that still have
WORKFLOW_NODE_EXECUTION, swaps those items to the right RESOURCE_CREDIT
prices (using existing Stripe schedule +
BillingSubscriptionUpdateService stack), then treats the workspace as V2
(flag). Workspaces without that legacy item or without a subscription
are skipped.

**4. Unify subscription lifecycle + usage on the server**

**5. Refresh the product surface in Settings**

Test : 

- [x] Subscribe v1 +  Update subscribe + Migrate
- [x]  Subscribe v2 + Update subscribe
This commit is contained in:
Etienne
2026-05-07 17:42:11 +02:00
committed by GitHub
parent ca58c7f15e
commit 9fc5be1c4c
93 changed files with 2939 additions and 514 deletions
@@ -1484,6 +1484,7 @@ enum BillingUsageType {
"""The different billing products available"""
enum BillingProductKey {
BASE_PRODUCT
RESOURCE_CREDIT
WORKFLOW_NODE_EXECUTION
}
@@ -1492,6 +1493,7 @@ type BillingPriceLicensed {
unitAmount: Float!
stripePriceId: String!
priceUsageType: BillingUsageType!
creditAmount: Float
}
enum SubscriptionInterval {
@@ -1590,7 +1592,8 @@ type BillingMeteredProductUsage {
type BillingPlan {
planKey: BillingPlanKey!
licensedProducts: [BillingLicensedProduct!]!
baseProducts: [BillingLicensedProduct!]!
resourceCreditProducts: [BillingLicensedProduct!]!
meteredProducts: [BillingMeteredProduct!]!
}
@@ -1754,6 +1757,7 @@ enum FeatureFlagKey {
IS_RICH_TEXT_V1_MIGRATED
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
IS_DATASOURCE_MIGRATED
IS_BILLING_V2_ENABLED
}
type WorkspaceUrls {
@@ -1145,13 +1145,14 @@ export type BillingUsageType = 'METERED' | 'LICENSED'
/** The different billing products available */
export type BillingProductKey = 'BASE_PRODUCT' | 'WORKFLOW_NODE_EXECUTION'
export type BillingProductKey = 'BASE_PRODUCT' | 'RESOURCE_CREDIT' | 'WORKFLOW_NODE_EXECUTION'
export interface BillingPriceLicensed {
recurringInterval: SubscriptionInterval
unitAmount: Scalars['Float']
stripePriceId: Scalars['String']
priceUsageType: BillingUsageType
creditAmount?: Scalars['Float']
__typename: 'BillingPriceLicensed'
}
@@ -1244,7 +1245,8 @@ export interface BillingMeteredProductUsage {
export interface BillingPlan {
planKey: BillingPlanKey
licensedProducts: BillingLicensedProduct[]
baseProducts: BillingLicensedProduct[]
resourceCreditProducts: BillingLicensedProduct[]
meteredProducts: BillingMeteredProduct[]
__typename: 'BillingPlan'
}
@@ -1386,7 +1388,7 @@ export interface FeatureFlag {
__typename: 'FeatureFlag'
}
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_DATASOURCE_MIGRATED'
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_DATASOURCE_MIGRATED' | 'IS_BILLING_V2_ENABLED'
export interface WorkspaceUrls {
customUrl?: Scalars['String']
@@ -4079,6 +4081,7 @@ export interface BillingPriceLicensedGenqlSelection{
unitAmount?: boolean | number
stripePriceId?: boolean | number
priceUsageType?: boolean | number
creditAmount?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -4177,7 +4180,8 @@ export interface BillingMeteredProductUsageGenqlSelection{
export interface BillingPlanGenqlSelection{
planKey?: boolean | number
licensedProducts?: BillingLicensedProductGenqlSelection
baseProducts?: BillingLicensedProductGenqlSelection
resourceCreditProducts?: BillingLicensedProductGenqlSelection
meteredProducts?: BillingMeteredProductGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
@@ -8629,6 +8633,7 @@ export const enumBillingUsageType = {
export const enumBillingProductKey = {
BASE_PRODUCT: 'BASE_PRODUCT' as const,
RESOURCE_CREDIT: 'RESOURCE_CREDIT' as const,
WORKFLOW_NODE_EXECUTION: 'WORKFLOW_NODE_EXECUTION' as const
}
@@ -8690,7 +8695,8 @@ export const enumFeatureFlagKey = {
IS_CONNECTED_ACCOUNT_MIGRATED: 'IS_CONNECTED_ACCOUNT_MIGRATED' as const,
IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const,
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const,
IS_DATASOURCE_MIGRATED: 'IS_DATASOURCE_MIGRATED' as const
IS_DATASOURCE_MIGRATED: 'IS_DATASOURCE_MIGRATED' as const,
IS_BILLING_V2_ENABLED: 'IS_BILLING_V2_ENABLED' as const
}
export const enumIdentityProviderType = {
@@ -2956,6 +2956,9 @@ export default {
"priceUsageType": [
136
],
"creditAmount": [
11
],
"__typename": [
1
]
@@ -3143,7 +3146,10 @@ export default {
"planKey": [
135
],
"licensedProducts": [
"baseProducts": [
143
],
"resourceCreditProducts": [
143
],
"meteredProducts": [