Skip to content

Commit d30faf2

Browse files
authored
fix(billing): the paid tier is Team, and it is not unlimited users (#7730)
Copy only. No behaviour, no lookup keys, no licence semantics, no backend. ## Current state Every surface that sells the paid self-hosted tier offers **"unlimited seats"** for **"$99/server/mo"**, and the portal's free plan badges **"Unlimited users"** and **"SSO included"** as free-tier facts. ## Problem Both claims are now enforceably false. [#7492](#7492) makes the licence carry a real user cap, and [Stirling-PDF-SaaS#325](Stirling-Tools/Stirling-PDF-SaaS#325) sells capacity in blocks of 100 users. An admin reading "unlimited seats" and then hitting a 409 at the invite screen is the worst version of this. The demo has already dropped both claims; ours were the last ones standing. ## Solution | Surface | Was | Now | |---|---|---| | Onboarding licence slide | "Stirling Server plan, **unlimited seats** … $99/server/mo" | "Stirling Team plan, **100 users** … $99/mo" | | Plan comparison table | `unlimitedUsers` = "Unlimited users" | `usersIncluded` = "100 users included" | | Plan card highlights | "Unlimited users" | "100 users included" | | Static plan section | `name: "Server"`, `maxUsers: "Unlimited users"` | `plan.team.name`, `plan.team.maxUsers` | | Upgrade banner | "Upgrade to Server Plan" / "unlimited users" | "Upgrade to the Team plan" / "100 users, SSO" | | Portal free plan | "Editor" + "SSO included" + "Unlimited users" | "Editor" + "Every PDF tool" + "Web, desktop & self-hosted" | The i18n keys are **renamed** (`unlimitedUsers` to `usersIncluded`) rather than just revalued, so the key name cannot outlive the claim. Also drops "per server" from `plan.licenseWarning` — we price a block of 100 users and count the provisioned roster, never nodes. And deletes the orphaned `[settings.planBilling.tier]` block: zero source references, and it described a retired model (50 credits/mo free, 500 included plus overage billing). ## Deliberately unchanged **"Processor" stays the name of the product surface.** The demo names each plan for its price tier (Editor = $0, Team = $99/mo, Credits = 1¢ each) while keeping Processor as the surface a plan unlocks. Renaming the surface here would conflate the two, so the plan-name split is left for the explicit plan catalogue. The free plan also gains no "500 free credits monthly" badge yet: that is true in the demo but not in our backend, which still grants a one-time lifetime pool. ## How to test Self-hosted, as an admin over the free user limit: Settings → Plan should offer the Team plan at "100 users included", and the onboarding licence slide should no longer promise unlimited seats. On the portal billing page, the free plan should read "Free" with no SSO or unlimited-users badge. Green locally: 4/4 i18n audits (missing, unused, structure, translation), 876 tests across 110 files, oxlint, prettier, and all four typecheck variants (core, proprietary, saas, portal).
1 parent f6661a8 commit d30faf2

9 files changed

Lines changed: 53 additions & 55 deletions

File tree

frontend/editor/public/locales/en-US/translation.toml

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5756,10 +5756,10 @@ rolePlaceholder = "Confirm your role"
57565756
roleUser = "User"
57575757

57585758
[onboarding.serverLicense]
5759-
freeBody = "Our <strong>Open-Core</strong> licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. To scale uninterrupted, we recommend the Stirling Server plan - <strong>unlimited seats</strong> and <strong>SSO support</strong> for $99/server/mo."
5760-
freeTitle = "Server License"
5761-
overLimitBody = "Our licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. You have <strong>{{overLimitUserCopy}}</strong> Stirling users. To continue uninterrupted, upgrade to the Stirling Server plan - <strong>unlimited seats</strong>, PDF text editing, and full admin control for $99/server/mo."
5762-
overLimitTitle = "Server License Needed"
5759+
freeBody = "Our <strong>Open-Core</strong> licensing permits up to <strong>{{freeTierLimit}}</strong> users for free. To scale uninterrupted, we recommend the Stirling Team plan - <strong>100 users</strong> and <strong>SSO support</strong> for $99/mo."
5760+
freeTitle = "Team plan"
5761+
overLimitBody = "Our licensing permits up to <strong>{{freeTierLimit}}</strong> users for free. You have <strong>{{overLimitUserCopy}}</strong> Stirling users. To continue uninterrupted, upgrade to the Stirling Team plan - <strong>100 users</strong>, PDF text editing, and full admin control for $99/mo."
5762+
overLimitTitle = "Team plan needed"
57635763
seePlans = "See Plans →"
57645764
upgrade = "Upgrade now →"
57655765

@@ -6570,7 +6570,7 @@ popular = "Popular"
65706570
selectPlan = "Select Plan"
65716571
showComparison = "Compare All Features"
65726572
upgrade = "Upgrade"
6573-
withServer = "+ Server Plan"
6573+
withServer = "+ Team plan"
65746574

65756575
[plan.api]
65766576
large = "5,000 Credits"
@@ -6588,8 +6588,8 @@ highlight1 = "Custom pricing"
65886588
highlight2 = "Dedicated support"
65896589
highlight3 = "Latest features"
65906590
name = "Enterprise"
6591-
requiresServer = "Requires Server"
6592-
requiresServerMessage = "Please upgrade to the Server plan first before upgrading to Enterprise."
6591+
requiresServer = "Requires Team plan"
6592+
requiresServerMessage = "Please upgrade to the Team plan first before upgrading to Enterprise."
65936593

65946594
[plan.feature]
65956595
api = "API Access"
@@ -6614,10 +6614,10 @@ saml = "SAML"
66146614
secureLoginSupport = "Secure Login Support"
66156615
selfHostedDeployment = "Self-hosted deployment"
66166616
sso = "SSO"
6617-
unlimitedUsers = "Unlimited users"
66186617
upToFiveUsers = "Up to 5 users"
66196618
upToFiveUsersLowercase = "up to 5 users"
66206619
usageTracking = "Usage tracking"
6620+
usersIncluded = "100 users included"
66216621
usersLimitedToSeats = "Users limited to seats"
66226622

66236623
[plan.free]
@@ -6646,12 +6646,12 @@ saveWithAnnualBilling = "Save with annual billing"
66466646
selfHosted = "Self-hosted"
66476647
selfHostedOnInfrastructure = "Self-hosted on your infrastructure"
66486648
ssoOAuth = "SSO (OAuth2/OIDC)"
6649-
unlimitedUsers = "Unlimited users"
66506649
upToFiveUsers = "Up to 5 users"
66516650
usageTrackingPrometheus = "Usage tracking & Prometheus"
6651+
usersIncluded = "100 users included"
66526652

66536653
[plan.licenseWarning]
6654-
body = "You have {{total}} users but the free tier only supports {{limit}} per server. Upgrade to keep Stirling PDF running smoothly."
6654+
body = "You have {{total}} users but the free tier only supports {{limit}}. Upgrade to keep Stirling PDF running smoothly."
66556655
cta = "See plans"
66566656
overLimit = "more than {{limit}}"
66576657
title = "Free self-hosted limit reached"
@@ -6674,7 +6674,7 @@ title = "You're on a Roll!"
66746674
[plan.static]
66756675
activateLicense = "Activate Your License"
66766676
contactToUpgrade = "Contact us to upgrade or customize your plan"
6677-
getLicense = "Get Server License"
6677+
getLicense = "Get the Team plan"
66786678
monthlyBilling = "Monthly Billing"
66796679
selectPeriod = "Select Billing Period"
66806680
upgradeToEnterprise = "Upgrade to Enterprise"
@@ -6695,6 +6695,10 @@ keyDescription = "Paste the license key from your email"
66956695
success = "License Activated!"
66966696
successMessage = "Your license has been successfully activated. You can now close this window."
66976697

6698+
[plan.team]
6699+
maxUsers = "100 users"
6700+
name = "Team"
6701+
66986702
[policies.activity]
66996703
outputsUnavailable = "Policy outputs are no longer available to download."
67006704
partialOutputsUnavailable = "Some policy outputs are no longer available to download."
@@ -7000,16 +7004,16 @@ subtitle = "Deploy anywhere, for your whole team."
70007004
title = "Free PDF Editors"
70017005

70027006
[portal.billing.freePlan]
7007+
anywhere = "Web, desktop & self-hosted"
70037008
checkoutErrorTitle = "Couldn't start checkout"
70047009
currentPlan = "Current plan"
7010+
everyPdfTool = "Every PDF tool"
70057011
freeForever = "Free forever"
70067012
noTeamResolved = "No team is resolved on your wallet yet — refresh and try again."
70077013
ownerOnly = "Only the team owner can switch on the Processor plan."
70087014
payInvoice = "Pay invoice to complete"
70097015
planName = "Editor"
7010-
ssoIncluded = "SSO included"
70117016
switchOnProcessor = "Switch on the Processor →"
7012-
unlimitedUsers = "Unlimited users"
70137017
viewQuote = "View quote"
70147018

70157019
[portal.billing.invoices]
@@ -10444,18 +10448,6 @@ memberCount_one = "{{count}} team member"
1044410448
memberCount_other = "{{count}} team members"
1044510449
memberCount_zero = "no team members"
1044610450

10447-
[settings.planBilling.tier]
10448-
enterprise = "Enterprise"
10449-
enterpriseDescription = "Custom enterprise features and support"
10450-
free = "Free"
10451-
freeDescription = "50 credits per month"
10452-
team = "Team"
10453-
teamBadge = "Team"
10454-
teamDescription = "500 credits/month included, automatic overage billing for uninterrupted service"
10455-
teamTooltipCredits = "Team plan includes {{credits}} credits/month."
10456-
teamTooltipFineprint = "Only pay for what you use beyond included credits."
10457-
teamTooltipOverage = "Automatic overage billing at {{price}}/credit ensures uninterrupted service."
10458-
1045910451
[settings.planBilling.trial]
1046010452
daysRemaining = "{{days}} days remaining"
1046110453
daysRemainingFull = "Your trial ends in {{days}} days"
@@ -11414,9 +11406,9 @@ urgent = "Urgent"
1141411406
attentionBody = "Your admin needs to sign in to see more info. Please contact them immediately."
1141511407
attentionBodyAdmin = "Review the license requirements to keep this server compliant."
1141611408
attentionTitle = "This server needs admin attention"
11417-
message = "Get the most out of Stirling PDF with unlimited users and advanced features"
11409+
message = "Get the most out of Stirling PDF with 100 users, SSO, and advanced features"
1141811410
seeInfo = "See info"
11419-
title = "Upgrade to Server Plan"
11411+
title = "Upgrade to the Team plan"
1142011412
upgradeButton = "Upgrade Now"
1142111413

1142211414
[URLToPDF]

frontend/editor/src/core/components/onboarding/slides/ServerLicenseSlide.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export default function ServerLicenseSlide({
2020
totalUsers != null ? totalUsers.toLocaleString() : null;
2121
const overLimitUserCopy = formattedTotalUsers ?? `more than ${freeTierLimit}`;
2222
const title = isOverLimit
23-
? i18n.t("onboarding.serverLicense.overLimitTitle", "Server License Needed")
24-
: i18n.t("onboarding.serverLicense.freeTitle", "Server License");
23+
? i18n.t("onboarding.serverLicense.overLimitTitle", "Team plan needed")
24+
: i18n.t("onboarding.serverLicense.freeTitle", "Team plan");
2525
const key = isOverLimit ? "server-license-over-limit" : "server-license";
2626

2727
const overLimitBody = (
@@ -31,7 +31,7 @@ export default function ServerLicenseSlide({
3131
components={{
3232
strong: <strong />,
3333
}}
34-
defaults="Our licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. You have <strong>{{overLimitUserCopy}}</strong> Stirling users. To continue uninterrupted, upgrade to the Stirling Server plan - <strong>unlimited seats</strong>, PDF text editing, and full admin control for $99/server/mo."
34+
defaults="Our licensing permits up to <strong>{{freeTierLimit}}</strong> users for free. You have <strong>{{overLimitUserCopy}}</strong> Stirling users. To continue uninterrupted, upgrade to the Stirling Team plan - <strong>100 users</strong>, PDF text editing, and full admin control for $99/mo."
3535
/>
3636
);
3737

@@ -42,7 +42,7 @@ export default function ServerLicenseSlide({
4242
components={{
4343
strong: <strong />,
4444
}}
45-
defaults="Our <strong>Open-Core</strong> licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. To scale uninterrupted, we recommend the Stirling Server plan - <strong>unlimited seats</strong> and <strong>SSO support</strong> for $99/server/mo."
45+
defaults="Our <strong>Open-Core</strong> licensing permits up to <strong>{{freeTierLimit}}</strong> users for free. To scale uninterrupted, we recommend the Stirling Team plan - <strong>100 users</strong> and <strong>SSO support</strong> for $99/mo."
4646
/>
4747
);
4848

frontend/editor/src/core/components/tools/certSign/CertificateSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const CertificateSelector: React.FC<CertificateSelectorProps> = ({
7070

7171
return (
7272
<Stack gap="md">
73-
{/* Managed certificate options — server plan only */}
73+
{/* Managed certificate options — Team plan only */}
7474
{isServerPlan && (
7575
<Radio.Group
7676
value={certType}

frontend/editor/src/portal/components/billing/FreePlanView.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,13 @@ export function FreePlanView({ wallet, unsynced, onSubscribed }: Props) {
114114
{t("portal.billing.freePlan.freeForever", "Free forever")}
115115
</StatusBadge>
116116
<StatusBadge tone="info" size="sm" showDot={false}>
117-
{t("portal.billing.freePlan.ssoIncluded", "SSO included")}
117+
{t("portal.billing.freePlan.everyPdfTool", "Every PDF tool")}
118118
</StatusBadge>
119119
<StatusBadge tone="purple" size="sm" showDot={false}>
120-
{t("portal.billing.freePlan.unlimitedUsers", "Unlimited users")}
120+
{t(
121+
"portal.billing.freePlan.anywhere",
122+
"Web, desktop & self-hosted",
123+
)}
121124
</StatusBadge>
122125
</div>
123126
</div>

frontend/editor/src/proprietary/components/shared/UpgradeBanner.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,10 +330,10 @@ const UpgradeBanner: React.FC = () => {
330330
{friendlyVisible && (
331331
<AppBanner
332332
icon="stars-rounded"
333-
title={t("upgradeBanner.title", "Upgrade to Server Plan")}
333+
title={t("upgradeBanner.title", "Upgrade to the Team plan")}
334334
message={t(
335335
"upgradeBanner.message",
336-
"Get the most out of Stirling PDF with unlimited users and advanced features.",
336+
"Get the most out of Stirling PDF with 100 users, SSO, and advanced features.",
337337
)}
338338
buttonText={t("upgradeBanner.upgradeButton", "Upgrade Now")}
339339
buttonIcon="upgrade-rounded"

frontend/editor/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,10 @@ const AdminPlanSection: React.FC = () => {
115115
if (currentTier === "free" && planGroup.tier === "enterprise") {
116116
alert({
117117
alertType: "warning",
118-
title: t("plan.enterprise.requiresServer", "Server Plan Required"),
118+
title: t("plan.enterprise.requiresServer", "Requires Team plan"),
119119
body: t(
120120
"plan.enterprise.requiresServerMessage",
121-
"Please upgrade to the Server plan first before upgrading to Enterprise.",
121+
"Please upgrade to the Team plan first before upgrading to Enterprise.",
122122
),
123123
});
124124
return;

frontend/editor/src/proprietary/components/shared/config/configSections/plan/PlanCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ const PlanCard: React.FC<PlanCardProps> = ({
144144
</Text>
145145
<Text size="sm" c="dimmed" mt="xs">
146146
{t("plan.perMonth", "/month")}{" "}
147-
{t("plan.withServer", "+ Server Plan")}
147+
{t("plan.withServer", "+ Team plan")}
148148
</Text>
149149
</>
150150
) : (
@@ -185,7 +185,7 @@ const PlanCard: React.FC<PlanCardProps> = ({
185185

186186
{/* Single Upgrade Button */}
187187
<Tooltip
188-
label={t("plan.enterprise.requiresServer", "Requires Server plan")}
188+
label={t("plan.enterprise.requiresServer", "Requires Team plan")}
189189
disabled={!isEnterpriseBlockedForFree}
190190
position="top"
191191
withArrow

frontend/editor/src/proprietary/components/shared/config/configSections/plan/StaticPlanSection.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
4848
if (currentTier === "free" && plan === "enterprise") {
4949
alert({
5050
alertType: "warning",
51-
title: t("plan.enterprise.requiresServer", "Server Plan Required"),
51+
title: t("plan.enterprise.requiresServer", "Requires Team plan"),
5252
body: t(
5353
"plan.enterprise.requiresServerMessage",
54-
"Please upgrade to the Server plan first before upgrading to Enterprise.",
54+
"Please upgrade to the Team plan first before upgrading to Enterprise.",
5555
),
5656
});
5757
return;
@@ -92,14 +92,14 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
9292
},
9393
{
9494
id: "server",
95-
name: "Server",
95+
name: t("plan.team.name", "Team"),
9696
price: 0,
9797
currency: "",
9898
period: "",
9999
popular: false,
100100
highlights: planHighlights.SERVER_MONTHLY,
101101
features: planFeatures.SERVER,
102-
maxUsers: "Unlimited users",
102+
maxUsers: t("plan.team.maxUsers", "100 users"),
103103
},
104104
{
105105
id: "enterprise",
@@ -227,7 +227,7 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
227227
);
228228
}
229229

230-
// Server Plan
230+
// Team plan
231231
if (plan.id === "server") {
232232
if (currentTier === "free") {
233233
return (
@@ -274,7 +274,7 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
274274
<Tooltip
275275
label={t(
276276
"plan.enterprise.requiresServer",
277-
"Requires Server plan",
277+
"Requires Team plan",
278278
)}
279279
position="top"
280280
withArrow

frontend/editor/src/proprietary/constants/planConstants.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ export const usePlanFeatures = (): PlanFeaturesMap => {
5050
"up to 5 users",
5151
);
5252
const upToFiveUsers = t("plan.features.upToFiveUsers", "Up to 5 users");
53-
const unlimitedUsers = t("plan.features.unlimitedUsers", "Unlimited users");
53+
const usersIncluded = t(
54+
"plan.features.usersIncluded",
55+
"100 users included",
56+
);
5457
const googleDriveIntegration = t(
5558
"plan.features.googleDriveIntegration",
5659
"Google drive integration",
@@ -88,7 +91,7 @@ export const usePlanFeatures = (): PlanFeaturesMap => {
8891
{ name: communitySupport, included: true },
8992
{ name: regularUpdates, included: true },
9093
{ name: upToFiveUsersLowercase, included: true },
91-
{ name: unlimitedUsers, included: false },
94+
{ name: usersIncluded, included: false },
9295
{ name: googleDriveIntegration, included: false },
9396
{ name: externalDatabase, included: false },
9497
{ name: editingTextInPdfs, included: false },
@@ -107,7 +110,7 @@ export const usePlanFeatures = (): PlanFeaturesMap => {
107110
{ name: communitySupport, included: true },
108111
{ name: regularUpdates, included: true },
109112
{ name: upToFiveUsers, included: false },
110-
{ name: unlimitedUsers, included: true },
113+
{ name: usersIncluded, included: true },
111114
{ name: googleDriveIntegration, included: true },
112115
{ name: externalDatabase, included: true },
113116
{ name: editingTextInPdfs, included: true },
@@ -126,7 +129,7 @@ export const usePlanFeatures = (): PlanFeaturesMap => {
126129
{ name: communitySupport, included: true },
127130
{ name: regularUpdates, included: true },
128131
{ name: upToFiveUsersLowercase, included: false },
129-
{ name: unlimitedUsers, included: false },
132+
{ name: usersIncluded, included: false },
130133
{ name: googleDriveIntegration, included: true },
131134
{ name: externalDatabase, included: true },
132135
{ name: editingTextInPdfs, included: true },
@@ -150,9 +153,9 @@ export const usePlanHighlights = (): PlanHighlightsMap => {
150153
"plan.highlights.selfHostedOnInfrastructure",
151154
"Self-hosted on your infrastructure",
152155
);
153-
const unlimitedUsers = t(
154-
"plan.highlights.unlimitedUsers",
155-
"Unlimited users",
156+
const usersIncluded = t(
157+
"plan.highlights.usersIncluded",
158+
"100 users included",
156159
);
157160
const advancedIntegrations = t(
158161
"plan.highlights.advancedIntegrations",
@@ -188,15 +191,15 @@ export const usePlanHighlights = (): PlanHighlightsMap => {
188191
],
189192
SERVER_MONTHLY: [
190193
selfHostedOnInfrastructure,
191-
unlimitedUsers,
194+
usersIncluded,
192195
advancedIntegrations,
193196
ssoOAuth,
194197
editingTextInPdfsCaps,
195198
t("plan.highlights.cancelAnytime", "Cancel anytime"),
196199
],
197200
SERVER_YEARLY: [
198201
selfHostedOnInfrastructure,
199-
unlimitedUsers,
202+
usersIncluded,
200203
advancedIntegrations,
201204
ssoOAuth,
202205
editingTextInPdfsCaps,

0 commit comments

Comments
 (0)