diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index 4003222e7..94d354643 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -7433,6 +7433,21 @@ "L4", "L17" ] + }, + { + "id": "msp-provider-tier-limit-lock", + "summary": "MSP provider workspace limits are intentionally tight: Starter includes up to 5 client workspaces, Growth up to 15, Scale up to 40, and larger providers move to Enterprise/custom terms so support, onboarding, reporting, and deployment load are sized before rollout.", + "kind": "pricing", + "decided_at": "2026-06-01", + "subsystem_ids": [ + "cloud-paid" + ], + "lane_ids": [ + "L2", + "L3", + "L4", + "L17" + ] } ] } diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index ffdd172a0..a9702cd01 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -239,7 +239,9 @@ or other self-hosted uncapped continuity plans. (`CP_MSP_STARTER_PRICE_ID`, `CP_MSP_GROWTH_PRICE_ID`, `CP_MSP_SCALE_PRICE_ID`) is still validated in `internal/cloudcp/config.go` against the canonical `msp_starter`, `msp_growth`, and `msp_scale` plan - versions, and the MSP signup routes stay gated behind the same + versions. The canonical provider workspace limits are 5 client workspaces + for Starter, 15 for Growth, and 40 for Scale; larger providers belong to + Enterprise/custom terms. The MSP signup routes stay gated behind the same `PublicCloudSignupEnabled` flag as the individual cloud signup front door. Checkout metadata produced by the MSP front door must mark `account_kind=msp`, `signup_source=public_msp_signup`, and diff --git a/internal/cloudcp/public_msp_signup_handlers.go b/internal/cloudcp/public_msp_signup_handlers.go index 852a08388..d8c4dfb72 100644 --- a/internal/cloudcp/public_msp_signup_handlers.go +++ b/internal/cloudcp/public_msp_signup_handlers.go @@ -147,8 +147,8 @@ var publicMSPSignupPageTemplate = template.Must(template.New("public-msp-signup-
-
Starter — up to 10 client workspaces, $149/mo
-
Growth / Scale — request access for 25+ client workspaces, custom onboarding, or assisted rollout.
+
Starter — up to 5 client workspaces, $149/mo
+
Growth / Scale — request access for 15+ client workspaces, custom onboarding, or assisted rollout.
diff --git a/internal/cloudcp/public_msp_signup_handlers_test.go b/internal/cloudcp/public_msp_signup_handlers_test.go index 82791897f..05c42753a 100644 --- a/internal/cloudcp/public_msp_signup_handlers_test.go +++ b/internal/cloudcp/public_msp_signup_handlers_test.go @@ -146,6 +146,12 @@ func TestMSPSignupPageKeepsStarterSelfServeWhenMultipleTiersConfigured(t *testin if !strings.Contains(body, "Growth / Scale") || !strings.Contains(body, "request access") { t.Fatal("expected assisted Growth/Scale copy") } + if !strings.Contains(body, "up to 5 client workspaces") { + t.Fatal("expected canonical MSP Starter workspace limit copy") + } + if !strings.Contains(body, "15+ client workspaces") { + t.Fatal("expected assisted MSP workspace threshold copy") + } } func TestMSPSignupPostValidRedirectsToStripeWithMSPMetadata(t *testing.T) { diff --git a/pkg/licensing/features.go b/pkg/licensing/features.go index 4cabb8cd0..7a64e9582 100644 --- a/pkg/licensing/features.go +++ b/pkg/licensing/features.go @@ -186,9 +186,9 @@ var CloudPlanWorkspaceLimits = map[string]int{ "cloud_founding": 1, // MSP tiers — client caps from pricing spec - "msp_starter": 10, // MSP Starter: up to 10 clients - "msp_growth": 25, // MSP Growth: up to 25 clients - "msp_scale": 50, // MSP Scale: up to 50 clients + "msp_starter": 5, // MSP Starter: up to 5 clients + "msp_growth": 15, // MSP Growth: up to 15 clients + "msp_scale": 40, // MSP Scale: up to 40 clients } // UnknownPlanDefaultWorkspaceLimit is the safe-default workspace limit applied diff --git a/pkg/licensing/features_test.go b/pkg/licensing/features_test.go index 403262ab3..98f22ba29 100644 --- a/pkg/licensing/features_test.go +++ b/pkg/licensing/features_test.go @@ -584,10 +584,10 @@ func TestWorkspaceLimitForPlan_KnownPlans(t *testing.T) { {"cloud_power", 1}, {"cloud_max", 1}, {"cloud_founding", 1}, - {"msp_starter", 10}, - {"msp_hosted_v1", 10}, - {"msp_growth", 25}, - {"msp_scale", 50}, + {"msp_starter", 5}, + {"msp_hosted_v1", 5}, + {"msp_growth", 15}, + {"msp_scale", 40}, } for _, tt := range tests { diff --git a/pkg/licensing/grant_claims_contract_test.go b/pkg/licensing/grant_claims_contract_test.go index acf01da4f..803492ea8 100644 --- a/pkg/licensing/grant_claims_contract_test.go +++ b/pkg/licensing/grant_claims_contract_test.go @@ -43,8 +43,8 @@ func TestMSPPlanAliasCanonicalizationContract(t *testing.T) { if got := CanonicalizePlanVersion("msp_hosted_v1"); got != "msp_starter" { t.Fatalf("CanonicalizePlanVersion(msp_hosted_v1) = %q, want %q", got, "msp_starter") } - if limit, known := WorkspaceLimitForPlan("msp_hosted_v1"); !known || limit != 10 { - t.Fatalf("WorkspaceLimitForPlan(msp_hosted_v1) = (%d, %v), want (10, true)", limit, known) + if limit, known := WorkspaceLimitForPlan("msp_hosted_v1"); !known || limit != 5 { + t.Fatalf("WorkspaceLimitForPlan(msp_hosted_v1) = (%d, %v), want (5, true)", limit, known) } } diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index c322c003e..54e1f4f1d 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -3800,6 +3800,7 @@ class SubsystemLookupTest(unittest.TestCase): "cloud-msp-stripe-prices", "legacy-grandfathering-eligibility-cutoff", "msp-buying-motion-lock", + "msp-provider-tier-limit-lock", "self-hosted-plans-surface-entitlement-first", "self-hosted-paid-surface-classification", "self-hosted-core-monitoring-free",