Tighten MSP workspace tier limits

This commit is contained in:
rcourtman 2026-06-01 16:55:23 +01:00
parent e450c6a4da
commit 914465e67f
8 changed files with 36 additions and 12 deletions

View file

@ -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"
]
}
]
}

View file

@ -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

View file

@ -147,8 +147,8 @@ var publicMSPSignupPageTemplate = template.Must(template.New("public-msp-signup-
<form method="POST" action="{{.FormAction}}">
<input type="hidden" name="tier" value="{{.Tier}}">
<div class="tier-group">
<div class="tier-option"><strong>Starter</strong> &mdash; up to 10 client workspaces, $149/mo</div>
<div class="tier-option"><strong>Growth / Scale</strong> &mdash; request access for 25+ client workspaces, custom onboarding, or assisted rollout.</div>
<div class="tier-option"><strong>Starter</strong> &mdash; up to 5 client workspaces, $149/mo</div>
<div class="tier-option"><strong>Growth / Scale</strong> &mdash; request access for 15+ client workspaces, custom onboarding, or assisted rollout.</div>
</div>
<label for="email">Work Email</label>

View file

@ -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) {

View file

@ -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

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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",