diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md
index bba23cf7d8..85761bffc0 100644
--- a/docs/release-control/v6/internal/subsystems/cloud-paid.md
+++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md
@@ -512,6 +512,10 @@ The same owner also holds shared hosted commercial copy such as page title,
introductory description, common Cloud inclusions, setup-step guidance,
hosted-signup section headings, and primary hosted-signup CTA labels when
those facts describe the canonical offer rather than one page's local layout.
+The same rule applies to self-hosted pricing framing: page title/description,
+badge copy, primary CTA labels, and adjacent upsell-link copy on
+`PricingV6.tsx` must come from the shared self-hosted plan-definition owner
+rather than page-local strings.
That same counted-unit boundary also owns the disclosure rule for retail copy:
default billing and pricing surfaces should use concise monitored-system copy,
while the full counted-unit definition appears only behind explicit disclosure
diff --git a/frontend-modern/src/pages/PricingV6.tsx b/frontend-modern/src/pages/PricingV6.tsx
index d09a0767fb..fc4f6db5b3 100644
--- a/frontend-modern/src/pages/PricingV6.tsx
+++ b/frontend-modern/src/pages/PricingV6.tsx
@@ -19,6 +19,7 @@ import { showToast } from '@/utils/toast';
import { logger } from '@/utils/logger';
import { getTrialStartErrorKind } from '@/utils/upgradePresentation';
import {
+ SELF_HOSTED_COMMERCIAL_PRESENTATION,
SELF_HOSTED_FEATURE_ROWS,
SELF_HOSTED_PLAN_BY_TIER,
type SelfHostedFeatureRow,
@@ -215,9 +216,9 @@ export default function PricingV6() {
const communityCta = createMemo(() => {
if (isCurrentTier('free')) {
- return Current Plan;
+ return {SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel};
}
- return Free;
+ return {SELF_HOSTED_COMMERCIAL_PRESENTATION.freeLabel};
});
// Tier rank for comparison — higher rank = higher tier.
@@ -236,11 +237,11 @@ export default function PricingV6() {
const relayCta = createMemo(() => {
if (isCurrentTier('relay')) {
- return Current Plan;
+ return {SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel};
}
// Users already on a higher tier don't need a Relay CTA.
if ((TIER_RANK[normalizedTier()] ?? 0) > (TIER_RANK['relay'] ?? 0)) {
- return Included;
+ return {SELF_HOSTED_COMMERCIAL_PRESENTATION.includedLabel};
}
return (
- Buy Relay
+ {SELF_HOSTED_COMMERCIAL_PRESENTATION.buyRelayLabel}
);
});
const proTrialOrUpgrade = createMemo(() => {
if (isCurrentTier('pro')) {
- return Current Plan;
+ return {SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel};
}
// Users already on a higher tier don't need a Pro CTA.
if ((TIER_RANK[normalizedTier()] ?? 0) > (TIER_RANK['pro'] ?? 0)) {
- return Included;
+ return {SELF_HOSTED_COMMERCIAL_PRESENTATION.includedLabel};
}
if (canStartTrial() && trialCtaMode() !== 'upgrade') {
return (
- Start Free 14-day Trial
+ {SELF_HOSTED_COMMERCIAL_PRESENTATION.startTrialLabel}
);
}
@@ -278,24 +279,24 @@ export default function PricingV6() {
target="_blank"
rel="noopener noreferrer"
>
- Upgrade to Pro
+ {SELF_HOSTED_COMMERCIAL_PRESENTATION.upgradeToProLabel}
);
});
const proPlusCta = createMemo(() => {
if (isCurrentTier('pro_plus')) {
- return Current Plan;
+ return {SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel};
}
// Users already on a higher tier don't need a Pro+ CTA.
if ((TIER_RANK[normalizedTier()] ?? 0) > (TIER_RANK['pro_plus'] ?? 0)) {
- return Included;
+ return {SELF_HOSTED_COMMERCIAL_PRESENTATION.includedLabel};
}
if (canStartTrial() && trialCtaMode() !== 'upgrade') {
return (
- Start Free 14-day Trial
+ {SELF_HOSTED_COMMERCIAL_PRESENTATION.startTrialLabel}
);
}
@@ -307,7 +308,7 @@ export default function PricingV6() {
target="_blank"
rel="noopener noreferrer"
>
- Upgrade to Pro+
+ {SELF_HOSTED_COMMERCIAL_PRESENTATION.upgradeToProPlusLabel}
);
});
@@ -316,7 +317,10 @@ export default function PricingV6() {
return (
-
+
{/* Tier cards — 4 columns on desktop, 2 on tablet, 1 on mobile */}
@@ -326,7 +330,7 @@ export default function PricingV6() {
tier={SELF_HOSTED_PLAN_BY_TIER.pro}
cta={proTrialOrUpgrade}
highlighted
- badge="Most Popular"
+ badge={SELF_HOSTED_COMMERCIAL_PRESENTATION.mostPopularBadge}
message={trialMessage()}
/>
@@ -334,35 +338,23 @@ export default function PricingV6() {
{/* Upsell links */}
-
- Need managed hosting?{' '}
-
- See Cloud plans
-
-
-
- Managing clients?{' '}
-
- See MSP plans
-
-
-
- Need more than 50 monitored systems?{' '}
-
- Contact us
-
-
+
+ {(link) => (
+
+ {link.preface}{' '}
+
+ {link.label}
+
+
+ )}
+
{/* Feature comparison table */}
- Feature Comparison
+
+ {SELF_HOSTED_COMMERCIAL_PRESENTATION.featureComparisonHeading}
+
diff --git a/frontend-modern/src/pages/__tests__/PricingV6.test.tsx b/frontend-modern/src/pages/__tests__/PricingV6.test.tsx
index 48642730ce..7de52ba90e 100644
--- a/frontend-modern/src/pages/__tests__/PricingV6.test.tsx
+++ b/frontend-modern/src/pages/__tests__/PricingV6.test.tsx
@@ -80,6 +80,12 @@ describe('PricingV6', () => {
expect(screen.getByText('15 monitored systems')).toBeInTheDocument();
expect(screen.getByText('50 monitored systems')).toBeInTheDocument();
expect(screen.getByText('Feature Comparison')).toBeInTheDocument();
+ expect(screen.getByText('Most Popular')).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: 'See Cloud plans' })).toHaveAttribute('href', '/cloud');
+ expect(screen.getByRole('link', { name: 'See MSP plans' })).toHaveAttribute(
+ 'href',
+ 'mailto:hello@pulserelay.pro?subject=Pulse%20MSP%20Inquiry',
+ );
expect(
screen.queryByText('Billing is based on monitored systems. Child resources are included.'),
).not.toBeInTheDocument();
diff --git a/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts b/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts
index c27d16ae99..d3af43f03c 100644
--- a/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts
+++ b/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
+ SELF_HOSTED_COMMERCIAL_PRESENTATION,
SELF_HOSTED_FEATURE_ROWS,
SELF_HOSTED_PLAN_BY_TIER,
SELF_HOSTED_PLAN_DEFINITIONS,
@@ -30,4 +31,37 @@ describe('selfHostedPlans', () => {
proPlus: '50',
});
});
+
+ it('keeps shared self-hosted commercial copy in the common contract', () => {
+ expect(SELF_HOSTED_COMMERCIAL_PRESENTATION).toEqual({
+ pageTitle: 'Pricing',
+ pageDescription: 'Compare tiers and choose what fits.',
+ mostPopularBadge: 'Most Popular',
+ currentPlanLabel: 'Current Plan',
+ includedLabel: 'Included',
+ freeLabel: 'Free',
+ buyRelayLabel: 'Buy Relay',
+ upgradeToProLabel: 'Upgrade to Pro',
+ upgradeToProPlusLabel: 'Upgrade to Pro+',
+ startTrialLabel: 'Start Free 14-day Trial',
+ featureComparisonHeading: 'Feature Comparison',
+ footerLinks: [
+ {
+ preface: 'Need managed hosting?',
+ label: 'See Cloud plans',
+ href: '/cloud',
+ },
+ {
+ preface: 'Managing clients?',
+ label: 'See MSP plans',
+ href: 'mailto:hello@pulserelay.pro?subject=Pulse%20MSP%20Inquiry',
+ },
+ {
+ preface: 'Need more than 50 monitored systems?',
+ label: 'Contact us',
+ href: 'mailto:hello@pulserelay.pro?subject=Pulse%20Enterprise%20Inquiry',
+ },
+ ],
+ });
+ });
});
diff --git a/frontend-modern/src/utils/selfHostedPlans.ts b/frontend-modern/src/utils/selfHostedPlans.ts
index 8849df6afb..8cb357fe48 100644
--- a/frontend-modern/src/utils/selfHostedPlans.ts
+++ b/frontend-modern/src/utils/selfHostedPlans.ts
@@ -19,6 +19,27 @@ export interface SelfHostedFeatureRow {
proPlus: boolean | string;
}
+export interface SelfHostedLinkCTA {
+ preface: string;
+ label: string;
+ href: string;
+}
+
+export interface SelfHostedCommercialPresentation {
+ pageTitle: string;
+ pageDescription: string;
+ mostPopularBadge: string;
+ currentPlanLabel: string;
+ includedLabel: string;
+ freeLabel: string;
+ buyRelayLabel: string;
+ upgradeToProLabel: string;
+ upgradeToProPlusLabel: string;
+ startTrialLabel: string;
+ featureComparisonHeading: string;
+ footerLinks: readonly SelfHostedLinkCTA[];
+}
+
export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] = [
{
tier: 'community',
@@ -88,6 +109,37 @@ export const SELF_HOSTED_PLAN_BY_TIER: Record