From d30faf246b7f169d83083eec20abc91e282857f2 Mon Sep 17 00:00:00 2001
From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Date: Tue, 1 Sep 2026 19:09:57 +0000
Subject: [PATCH] fix(billing): the paid tier is Team, and it is not unlimited
users (#7730)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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](https://github.com/Stirling-Tools/Stirling-PDF/pull/7492) makes
the licence carry a real user cap, and
[Stirling-PDF-SaaS#325](https://github.com/Stirling-Tools/Stirling-PDF-SaaS/pull/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).
---
.../public/locales/en-US/translation.toml | 46 ++++++++-----------
.../onboarding/slides/ServerLicenseSlide.tsx | 8 ++--
.../tools/certSign/CertificateSelector.tsx | 2 +-
.../components/billing/FreePlanView.tsx | 7 ++-
.../components/shared/UpgradeBanner.tsx | 4 +-
.../configSections/AdminPlanSection.tsx | 4 +-
.../config/configSections/plan/PlanCard.tsx | 4 +-
.../configSections/plan/StaticPlanSection.tsx | 12 ++---
.../proprietary/constants/planConstants.ts | 21 +++++----
9 files changed, 53 insertions(+), 55 deletions(-)
diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml
index d4a028388b..eea64d65b8 100644
--- a/frontend/editor/public/locales/en-US/translation.toml
+++ b/frontend/editor/public/locales/en-US/translation.toml
@@ -5756,10 +5756,10 @@ rolePlaceholder = "Confirm your role"
roleUser = "User"
[onboarding.serverLicense]
-freeBody = "Our Open-Core licensing permits up to {{freeTierLimit}} users for free per server. To scale uninterrupted, we recommend the Stirling Server plan - unlimited seats and SSO support for $99/server/mo."
-freeTitle = "Server License"
-overLimitBody = "Our licensing permits up to {{freeTierLimit}} users for free per server. You have {{overLimitUserCopy}} Stirling users. To continue uninterrupted, upgrade to the Stirling Server plan - unlimited seats, PDF text editing, and full admin control for $99/server/mo."
-overLimitTitle = "Server License Needed"
+freeBody = "Our Open-Core licensing permits up to {{freeTierLimit}} users for free. To scale uninterrupted, we recommend the Stirling Team plan - 100 users and SSO support for $99/mo."
+freeTitle = "Team plan"
+overLimitBody = "Our licensing permits up to {{freeTierLimit}} users for free. You have {{overLimitUserCopy}} Stirling users. To continue uninterrupted, upgrade to the Stirling Team plan - 100 users, PDF text editing, and full admin control for $99/mo."
+overLimitTitle = "Team plan needed"
seePlans = "See Plans →"
upgrade = "Upgrade now →"
@@ -6570,7 +6570,7 @@ popular = "Popular"
selectPlan = "Select Plan"
showComparison = "Compare All Features"
upgrade = "Upgrade"
-withServer = "+ Server Plan"
+withServer = "+ Team plan"
[plan.api]
large = "5,000 Credits"
@@ -6588,8 +6588,8 @@ highlight1 = "Custom pricing"
highlight2 = "Dedicated support"
highlight3 = "Latest features"
name = "Enterprise"
-requiresServer = "Requires Server"
-requiresServerMessage = "Please upgrade to the Server plan first before upgrading to Enterprise."
+requiresServer = "Requires Team plan"
+requiresServerMessage = "Please upgrade to the Team plan first before upgrading to Enterprise."
[plan.feature]
api = "API Access"
@@ -6614,10 +6614,10 @@ saml = "SAML"
secureLoginSupport = "Secure Login Support"
selfHostedDeployment = "Self-hosted deployment"
sso = "SSO"
-unlimitedUsers = "Unlimited users"
upToFiveUsers = "Up to 5 users"
upToFiveUsersLowercase = "up to 5 users"
usageTracking = "Usage tracking"
+usersIncluded = "100 users included"
usersLimitedToSeats = "Users limited to seats"
[plan.free]
@@ -6646,12 +6646,12 @@ saveWithAnnualBilling = "Save with annual billing"
selfHosted = "Self-hosted"
selfHostedOnInfrastructure = "Self-hosted on your infrastructure"
ssoOAuth = "SSO (OAuth2/OIDC)"
-unlimitedUsers = "Unlimited users"
upToFiveUsers = "Up to 5 users"
usageTrackingPrometheus = "Usage tracking & Prometheus"
+usersIncluded = "100 users included"
[plan.licenseWarning]
-body = "You have {{total}} users but the free tier only supports {{limit}} per server. Upgrade to keep Stirling PDF running smoothly."
+body = "You have {{total}} users but the free tier only supports {{limit}}. Upgrade to keep Stirling PDF running smoothly."
cta = "See plans"
overLimit = "more than {{limit}}"
title = "Free self-hosted limit reached"
@@ -6674,7 +6674,7 @@ title = "You're on a Roll!"
[plan.static]
activateLicense = "Activate Your License"
contactToUpgrade = "Contact us to upgrade or customize your plan"
-getLicense = "Get Server License"
+getLicense = "Get the Team plan"
monthlyBilling = "Monthly Billing"
selectPeriod = "Select Billing Period"
upgradeToEnterprise = "Upgrade to Enterprise"
@@ -6695,6 +6695,10 @@ keyDescription = "Paste the license key from your email"
success = "License Activated!"
successMessage = "Your license has been successfully activated. You can now close this window."
+[plan.team]
+maxUsers = "100 users"
+name = "Team"
+
[policies.activity]
outputsUnavailable = "Policy outputs are no longer available to download."
partialOutputsUnavailable = "Some policy outputs are no longer available to download."
@@ -7000,16 +7004,16 @@ subtitle = "Deploy anywhere, for your whole team."
title = "Free PDF Editors"
[portal.billing.freePlan]
+anywhere = "Web, desktop & self-hosted"
checkoutErrorTitle = "Couldn't start checkout"
currentPlan = "Current plan"
+everyPdfTool = "Every PDF tool"
freeForever = "Free forever"
noTeamResolved = "No team is resolved on your wallet yet — refresh and try again."
ownerOnly = "Only the team owner can switch on the Processor plan."
payInvoice = "Pay invoice to complete"
planName = "Editor"
-ssoIncluded = "SSO included"
switchOnProcessor = "Switch on the Processor →"
-unlimitedUsers = "Unlimited users"
viewQuote = "View quote"
[portal.billing.invoices]
@@ -10444,18 +10448,6 @@ memberCount_one = "{{count}} team member"
memberCount_other = "{{count}} team members"
memberCount_zero = "no team members"
-[settings.planBilling.tier]
-enterprise = "Enterprise"
-enterpriseDescription = "Custom enterprise features and support"
-free = "Free"
-freeDescription = "50 credits per month"
-team = "Team"
-teamBadge = "Team"
-teamDescription = "500 credits/month included, automatic overage billing for uninterrupted service"
-teamTooltipCredits = "Team plan includes {{credits}} credits/month."
-teamTooltipFineprint = "Only pay for what you use beyond included credits."
-teamTooltipOverage = "Automatic overage billing at {{price}}/credit ensures uninterrupted service."
-
[settings.planBilling.trial]
daysRemaining = "{{days}} days remaining"
daysRemainingFull = "Your trial ends in {{days}} days"
@@ -11414,9 +11406,9 @@ urgent = "Urgent"
attentionBody = "Your admin needs to sign in to see more info. Please contact them immediately."
attentionBodyAdmin = "Review the license requirements to keep this server compliant."
attentionTitle = "This server needs admin attention"
-message = "Get the most out of Stirling PDF with unlimited users and advanced features"
+message = "Get the most out of Stirling PDF with 100 users, SSO, and advanced features"
seeInfo = "See info"
-title = "Upgrade to Server Plan"
+title = "Upgrade to the Team plan"
upgradeButton = "Upgrade Now"
[URLToPDF]
diff --git a/frontend/editor/src/core/components/onboarding/slides/ServerLicenseSlide.tsx b/frontend/editor/src/core/components/onboarding/slides/ServerLicenseSlide.tsx
index ac32244e85..09265fb3cf 100644
--- a/frontend/editor/src/core/components/onboarding/slides/ServerLicenseSlide.tsx
+++ b/frontend/editor/src/core/components/onboarding/slides/ServerLicenseSlide.tsx
@@ -20,8 +20,8 @@ export default function ServerLicenseSlide({
totalUsers != null ? totalUsers.toLocaleString() : null;
const overLimitUserCopy = formattedTotalUsers ?? `more than ${freeTierLimit}`;
const title = isOverLimit
- ? i18n.t("onboarding.serverLicense.overLimitTitle", "Server License Needed")
- : i18n.t("onboarding.serverLicense.freeTitle", "Server License");
+ ? i18n.t("onboarding.serverLicense.overLimitTitle", "Team plan needed")
+ : i18n.t("onboarding.serverLicense.freeTitle", "Team plan");
const key = isOverLimit ? "server-license-over-limit" : "server-license";
const overLimitBody = (
@@ -31,7 +31,7 @@ export default function ServerLicenseSlide({
components={{
strong: ,
}}
- defaults="Our licensing permits up to {{freeTierLimit}} users for free per server. You have {{overLimitUserCopy}} Stirling users. To continue uninterrupted, upgrade to the Stirling Server plan - unlimited seats, PDF text editing, and full admin control for $99/server/mo."
+ defaults="Our licensing permits up to {{freeTierLimit}} users for free. You have {{overLimitUserCopy}} Stirling users. To continue uninterrupted, upgrade to the Stirling Team plan - 100 users, PDF text editing, and full admin control for $99/mo."
/>
);
@@ -42,7 +42,7 @@ export default function ServerLicenseSlide({
components={{
strong: ,
}}
- defaults="Our Open-Core licensing permits up to {{freeTierLimit}} users for free per server. To scale uninterrupted, we recommend the Stirling Server plan - unlimited seats and SSO support for $99/server/mo."
+ defaults="Our Open-Core licensing permits up to {{freeTierLimit}} users for free. To scale uninterrupted, we recommend the Stirling Team plan - 100 users and SSO support for $99/mo."
/>
);
diff --git a/frontend/editor/src/core/components/tools/certSign/CertificateSelector.tsx b/frontend/editor/src/core/components/tools/certSign/CertificateSelector.tsx
index 0c09b8c5e6..0cccbe4a40 100644
--- a/frontend/editor/src/core/components/tools/certSign/CertificateSelector.tsx
+++ b/frontend/editor/src/core/components/tools/certSign/CertificateSelector.tsx
@@ -70,7 +70,7 @@ export const CertificateSelector: React.FC = ({
return (
- {/* Managed certificate options — server plan only */}
+ {/* Managed certificate options — Team plan only */}
{isServerPlan && (
- {t("portal.billing.freePlan.ssoIncluded", "SSO included")}
+ {t("portal.billing.freePlan.everyPdfTool", "Every PDF tool")}
- {t("portal.billing.freePlan.unlimitedUsers", "Unlimited users")}
+ {t(
+ "portal.billing.freePlan.anywhere",
+ "Web, desktop & self-hosted",
+ )}
diff --git a/frontend/editor/src/proprietary/components/shared/UpgradeBanner.tsx b/frontend/editor/src/proprietary/components/shared/UpgradeBanner.tsx
index fdb92dc271..87d44000d2 100644
--- a/frontend/editor/src/proprietary/components/shared/UpgradeBanner.tsx
+++ b/frontend/editor/src/proprietary/components/shared/UpgradeBanner.tsx
@@ -330,10 +330,10 @@ const UpgradeBanner: React.FC = () => {
{friendlyVisible && (
{
if (currentTier === "free" && planGroup.tier === "enterprise") {
alert({
alertType: "warning",
- title: t("plan.enterprise.requiresServer", "Server Plan Required"),
+ title: t("plan.enterprise.requiresServer", "Requires Team plan"),
body: t(
"plan.enterprise.requiresServerMessage",
- "Please upgrade to the Server plan first before upgrading to Enterprise.",
+ "Please upgrade to the Team plan first before upgrading to Enterprise.",
),
});
return;
diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/plan/PlanCard.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/plan/PlanCard.tsx
index 1bf1dd0d76..e45e29e50c 100644
--- a/frontend/editor/src/proprietary/components/shared/config/configSections/plan/PlanCard.tsx
+++ b/frontend/editor/src/proprietary/components/shared/config/configSections/plan/PlanCard.tsx
@@ -144,7 +144,7 @@ const PlanCard: React.FC = ({
{t("plan.perMonth", "/month")}{" "}
- {t("plan.withServer", "+ Server Plan")}
+ {t("plan.withServer", "+ Team plan")}
>
) : (
@@ -185,7 +185,7 @@ const PlanCard: React.FC = ({
{/* Single Upgrade Button */}
= ({
if (currentTier === "free" && plan === "enterprise") {
alert({
alertType: "warning",
- title: t("plan.enterprise.requiresServer", "Server Plan Required"),
+ title: t("plan.enterprise.requiresServer", "Requires Team plan"),
body: t(
"plan.enterprise.requiresServerMessage",
- "Please upgrade to the Server plan first before upgrading to Enterprise.",
+ "Please upgrade to the Team plan first before upgrading to Enterprise.",
),
});
return;
@@ -92,14 +92,14 @@ const StaticPlanSection: React.FC = ({
},
{
id: "server",
- name: "Server",
+ name: t("plan.team.name", "Team"),
price: 0,
currency: "",
period: "",
popular: false,
highlights: planHighlights.SERVER_MONTHLY,
features: planFeatures.SERVER,
- maxUsers: "Unlimited users",
+ maxUsers: t("plan.team.maxUsers", "100 users"),
},
{
id: "enterprise",
@@ -227,7 +227,7 @@ const StaticPlanSection: React.FC = ({
);
}
- // Server Plan
+ // Team plan
if (plan.id === "server") {
if (currentTier === "free") {
return (
@@ -274,7 +274,7 @@ const StaticPlanSection: React.FC = ({
{
"up to 5 users",
);
const upToFiveUsers = t("plan.features.upToFiveUsers", "Up to 5 users");
- const unlimitedUsers = t("plan.features.unlimitedUsers", "Unlimited users");
+ const usersIncluded = t(
+ "plan.features.usersIncluded",
+ "100 users included",
+ );
const googleDriveIntegration = t(
"plan.features.googleDriveIntegration",
"Google drive integration",
@@ -88,7 +91,7 @@ export const usePlanFeatures = (): PlanFeaturesMap => {
{ name: communitySupport, included: true },
{ name: regularUpdates, included: true },
{ name: upToFiveUsersLowercase, included: true },
- { name: unlimitedUsers, included: false },
+ { name: usersIncluded, included: false },
{ name: googleDriveIntegration, included: false },
{ name: externalDatabase, included: false },
{ name: editingTextInPdfs, included: false },
@@ -107,7 +110,7 @@ export const usePlanFeatures = (): PlanFeaturesMap => {
{ name: communitySupport, included: true },
{ name: regularUpdates, included: true },
{ name: upToFiveUsers, included: false },
- { name: unlimitedUsers, included: true },
+ { name: usersIncluded, included: true },
{ name: googleDriveIntegration, included: true },
{ name: externalDatabase, included: true },
{ name: editingTextInPdfs, included: true },
@@ -126,7 +129,7 @@ export const usePlanFeatures = (): PlanFeaturesMap => {
{ name: communitySupport, included: true },
{ name: regularUpdates, included: true },
{ name: upToFiveUsersLowercase, included: false },
- { name: unlimitedUsers, included: false },
+ { name: usersIncluded, included: false },
{ name: googleDriveIntegration, included: true },
{ name: externalDatabase, included: true },
{ name: editingTextInPdfs, included: true },
@@ -150,9 +153,9 @@ export const usePlanHighlights = (): PlanHighlightsMap => {
"plan.highlights.selfHostedOnInfrastructure",
"Self-hosted on your infrastructure",
);
- const unlimitedUsers = t(
- "plan.highlights.unlimitedUsers",
- "Unlimited users",
+ const usersIncluded = t(
+ "plan.highlights.usersIncluded",
+ "100 users included",
);
const advancedIntegrations = t(
"plan.highlights.advancedIntegrations",
@@ -188,7 +191,7 @@ export const usePlanHighlights = (): PlanHighlightsMap => {
],
SERVER_MONTHLY: [
selfHostedOnInfrastructure,
- unlimitedUsers,
+ usersIncluded,
advancedIntegrations,
ssoOAuth,
editingTextInPdfsCaps,
@@ -196,7 +199,7 @@ export const usePlanHighlights = (): PlanHighlightsMap => {
],
SERVER_YEARLY: [
selfHostedOnInfrastructure,
- unlimitedUsers,
+ usersIncluded,
advancedIntegrations,
ssoOAuth,
editingTextInPdfsCaps,