Clarify paid Pulse Pro runtime install path

This commit is contained in:
rcourtman 2026-05-06 21:52:50 +01:00
parent 1f424523da
commit fc4928e1f2
16 changed files with 168 additions and 18 deletions

View file

@ -70,6 +70,12 @@ Power-user shortcuts:
## ⚡ Quick Start
> **Paid Pulse Pro / Relay / legacy customers:** GitHub release assets and the
> public `rcourtman/pulse` Docker image are community builds. They can accept an
> activation key, but they do not include the private Pulse Pro runtime hooks.
> Use <https://pulserelay.pro/download.html> with your activation key to get the
> private Pulse Pro Docker image or Linux archive.
### Option 1: Proxmox LXC (Recommended)
Replace `vX.Y.Z` with the exact release tag you want, verify the signed installer, then run it on your Proxmox host:

View file

@ -2,6 +2,13 @@
Pulse is distributed as a lightweight, Alpine-based Docker image.
> **Paid Pulse Pro / Relay / legacy customers:** The public `rcourtman/pulse`
> Docker image is the community build. It can accept an activation key, but it
> does not include the private Pulse Pro runtime hooks. Use
> <https://pulserelay.pro/download.html> with your activation key, then run the
> private registry login and `PULSE_IMAGE=license.pulserelay.pro/pulse-pro:<version>`
> compose commands shown there.
## 🚀 Quick Start
```bash
@ -172,6 +179,12 @@ docker login registry.example.com
The agent uses the Docker daemon's credentials for both pulling images and checking for updates.
Paid Pulse Pro Docker installs use the private Pulse Pro registry rather than
the public `rcourtman/pulse` image. Open <https://pulserelay.pro/download.html>,
paste your activation key, run the Docker login command shown there, then run
the shown `PULSE_IMAGE=license.pulserelay.pro/pulse-pro:<version> docker compose pull`
and `docker compose up -d` commands from the host that already runs Pulse.
### Disabling Update Features
Pulse provides granular control over update features via environment variables on the **Pulse server**:

View file

@ -2,6 +2,12 @@
Pulse offers flexible installation options from Docker to enterprise-ready Kubernetes charts.
> **Paid Pulse Pro / Relay / legacy customers:** GitHub release assets and the
> public `rcourtman/pulse` Docker image are community builds. They can accept an
> activation key, but they do not include the private Pulse Pro runtime hooks.
> Use <https://pulserelay.pro/download.html> with your activation key to get the
> private Pulse Pro Docker image or Linux archive.
## 🚀 Quick Start (Recommended)
### Proxmox VE (LXC installer)

View file

@ -140,6 +140,19 @@ activation, recovery, or BYOK/local AI setup instead; any exceptional
support-issued entitlement is reflected through hosted entitlement state rather
than a local in-app trial acquisition flow.
#### Paid Pulse Pro Runtime
Paid Pulse Pro, Relay, and eligible legacy customers should not use public
GitHub release assets or the public `rcourtman/pulse` Docker image for paid
runtime features. Those public downloads are community builds. They can accept
an activation key, but they do not include the private Pulse Pro runtime hooks.
Use <https://pulserelay.pro/download.html> with your activation key instead.
Docker users should run the private registry login and
`PULSE_IMAGE=license.pulserelay.pro/pulse-pro:<version>` compose commands shown
there. Direct Linux users should download the private Pulse Pro archive from the
same page.
#### v5 License Migration
Pulse v6 uses the activation/grant model for active licensing, but it can migrate valid Pulse v5 paid JWT-style licenses, including legacy Pro and Lifetime licenses.

View file

@ -701,6 +701,10 @@ blocked when the process is the public community build. Customer-facing paid
runtime copy must send those users to the private Pulse Pro download rather
than implying that the public GitHub release or public Docker image can unlock
the same runtime hooks.
The owned self-hosted Plans surface must also show a direct Pulse Pro downloads
action when an active paid entitlement is running on the community runtime, and
the existing-key recovery panel must warn that paid Docker and Linux installs
need the private Pulse Pro image or archive before a customer pastes a key.
That same browser-shell boundary also owns utility-nav suppression:
`frontend-modern/src/AppLayout.tsx` must not expose a separate top-level
Operations destination. Diagnostics, reports, and logs now live under the

View file

@ -219,9 +219,14 @@ server-side update execution surfaces.
Paid-user GA is part of that same release boundary: the public Pulse release
workflow builds OSS `pulse-v...` artifacts only, so release docs and runbooks
must require a same-ref/same-version `pulse-enterprise` Pro package, verify
`pulse-pro-v...` archives identify `Pulse Pro (Enterprise)`, and keep the
paid install/upgrade path pointed at Pro artifacts or a verified paid image
before any paid-user Pro runtime claim is made.
`pulse-pro-v...` archives identify `Pulse Pro`, and keep the paid
install/upgrade path pointed at Pro artifacts or a verified paid image before
any paid-user Pro runtime claim is made. Public GitHub release assets and the
public `rcourtman/pulse` Docker image must be described as community builds
where paid customers are likely to install or upgrade, and generated public
release bodies must send Relay, Pulse Pro, and eligible legacy customers to
`https://pulserelay.pro/download.html` for the private Pulse Pro Docker image
or Linux archive.
The repo-root VERSION file is part of the same governed boundary and must
not drift as an
unowned release-cut switch: changing the version string for a new RC or

View file

@ -84,7 +84,7 @@ The workflow auto-marks `-rc.N`/`-alpha.N`/`-beta.N` as prerelease.
other explicit trust-migration path before testing those newer packets.
6. For paid-user GA, run the `pulse-enterprise` Pro release workflow against the
same Pulse ref/version, verify `pulse-pro-v...` archives exist, verify
`bin/pulse --version` identifies `Pulse Pro (Enterprise)`, and confirm the paid
`bin/pulse --version` identifies `Pulse Pro`, and confirm the paid
install/upgrade docs point paid customers to the Pro artifacts or verified paid
container image.

View file

@ -28,6 +28,8 @@ interface ProLicensePlanSectionProps {
tone: string;
highlightsLabel: string;
highlights: string[];
actionLabel?: string;
actionUrl?: string;
} | null;
activationProof: {
title: string;
@ -55,6 +57,10 @@ interface ProLicensePlanSectionProps {
includedExtras: string[];
supplementalBadges: string[];
supplementalSummary?: string;
privateRuntimeAction?: {
actionLabel: string;
actionUrl: string;
};
};
entitlements: {
in_grace_period?: boolean;
@ -109,6 +115,16 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
<div class={`mb-4 rounded-md border p-3 text-sm ${summary().tone}`}>
<p class="font-medium">{summary().title}</p>
<p class="mt-1 text-xs opacity-90">{summary().body}</p>
<Show when={summary().actionUrl && summary().actionLabel}>
<a
href={summary().actionUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 mt-3 min-h-10 sm:min-h-9 rounded-md border border-current/20 px-3 py-2 text-xs font-medium hover:bg-black/5 dark:hover:bg-white/5"
>
{summary().actionLabel}
</a>
</Show>
<Show when={summary().highlights.length > 0}>
<div class="mt-3">
<p class="text-[11px] uppercase tracking-wide opacity-80">
@ -186,6 +202,18 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
<Show when={props.currentPlanSummary.supplementalSummary}>
{(summary) => <p class="mt-2 text-xs text-muted">{summary()}</p>}
</Show>
<Show when={props.currentPlanSummary.privateRuntimeAction}>
{(action) => (
<a
href={action().actionUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 mt-3 min-h-10 sm:min-h-9 rounded-md border border-border px-3 py-2 text-xs font-medium text-base-content hover:bg-surface-hover"
>
{action().actionLabel}
</a>
)}
</Show>
<Show when={props.currentPlanSummary.unlockedFeatures.length > 0}>
<div class="mt-4">
<p class="text-xs uppercase tracking-wide text-muted mb-2">
@ -326,9 +354,7 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
<Show
when={props.hasLicenseDetails}
fallback={
<div class="text-sm text-muted">
{getNoActiveSelfHostedActivationState().text}
</div>
<div class="text-sm text-muted">{getNoActiveSelfHostedActivationState().text}</div>
}
>
<CommercialStatGrid items={props.commercialPlanModel.summary} />
@ -343,9 +369,7 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
)}
</For>
</div>
</Show>
</Show>
</Show>
</>

View file

@ -67,6 +67,22 @@ export const SelfHostedCommercialRecoverySection: Component<
</a>
{SELF_HOSTED_RECOVERY_PRESENTATION.helpTextAfterTerms}
</p>
<div class="mt-3 rounded-md border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 p-3 text-sm text-amber-800 dark:text-amber-200">
<p class="font-medium">
{SELF_HOSTED_RECOVERY_PRESENTATION.privateRuntimeNotice.title}
</p>
<p class="mt-1 text-xs text-amber-700 dark:text-amber-300">
{SELF_HOSTED_RECOVERY_PRESENTATION.privateRuntimeNotice.body}
</p>
<a
href={SELF_HOSTED_RECOVERY_PRESENTATION.privateRuntimeNotice.actionUrl}
target="_blank"
rel="noopener noreferrer"
class="mt-2 inline-flex min-h-10 sm:min-h-9 items-center rounded px-1 text-xs font-medium text-blue-700 dark:text-blue-300 hover:underline"
>
{SELF_HOSTED_RECOVERY_PRESENTATION.privateRuntimeNotice.actionLabel}
</a>
</div>
<Show when={props.looksLikeLegacyLicenseKey}>
<div class="mt-3 rounded-md border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 p-3 text-sm text-amber-800 dark:text-amber-200">
<p class="font-medium">{SELF_HOSTED_RECOVERY_PRESENTATION.legacyNotice.title}</p>

View file

@ -499,7 +499,7 @@ describe('ProLicensePanel', () => {
expect(screen.getByText('Pulse Pro value proof')).toBeInTheDocument();
expect(
screen.getByText(
"These checks come from this instance's entitlement payload, not from public pricing copy.",
"These checks come from this instance's entitlement and runtime payloads, not from public pricing copy.",
),
).toBeInTheDocument();
expect(screen.getByText('Remote access, pairing, and push')).toBeInTheDocument();
@ -1118,7 +1118,9 @@ describe('ProLicensePanel', () => {
expect(proLicensePlanSectionSource).not.toContain('getTrialEndedProLicenseNotice');
expect(proLicensePlanSectionSource).toContain('currentPlanSummary.title');
expect(proLicensePlanSectionSource).toContain('currentPlanSummary.supplementalBadges');
expect(proLicensePlanSectionSource).toContain('currentPlanSummary.privateRuntimeAction');
expect(proLicensePlanSectionSource).toContain('props.activationSuccessSummary');
expect(proLicensePlanSectionSource).toContain('summary().actionUrl');
expect(proLicensePlanSectionSource).toContain('props.planComparisonSummary.cards.length > 0');
expect(proLicensePlanSectionSource).toContain(
'SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonSectionTitle',
@ -1145,6 +1147,7 @@ describe('ProLicensePanel', () => {
);
expect(selfHostedCommercialRecoverySectionSource).toContain('TERMS_DOC_URL');
expect(selfHostedCommercialRecoverySectionSource).toContain('disclosureLabel');
expect(selfHostedCommercialRecoverySectionSource).toContain('privateRuntimeNotice');
expect(selfHostedCommercialRecoverySectionSource).toContain('recoverySectionTitle');
expect(selfHostedCommercialRecoverySectionSource).not.toContain(
'https://github.com/rcourtman/Pulse/blob/main/TERMS.md',

View file

@ -53,6 +53,11 @@ describe('licensePresentation', () => {
fieldLabel: 'License or Activation Key',
activateIdleLabel: 'Activate Key',
clearIdleLabel: 'Clear Key',
privateRuntimeNotice: {
title: 'Paid Docker and Linux installs use a private runtime',
actionLabel: 'Open Pulse Pro downloads',
actionUrl: 'https://pulserelay.pro/download.html',
},
legacyNotice: {
title: 'Legacy v5 license detected',
},
@ -369,6 +374,10 @@ describe('licensePresentation', () => {
title: 'Current plan: Pulse Pro',
body: expect.stringContaining('private Pulse Pro runtime'),
supplementalBadges: ['Pro runtime missing'],
privateRuntimeAction: {
actionLabel: 'Open Pulse Pro downloads',
actionUrl: 'https://pulserelay.pro/download.html',
},
});
expect(getSelfHostedActivationProofPresentation(entitlements)?.items[0]).toMatchObject({
label: 'Pulse Pro runtime',
@ -387,6 +396,8 @@ describe('licensePresentation', () => {
tone: expect.stringContaining('amber'),
body: expect.stringContaining('community runtime'),
highlightsLabel: 'Licensed capabilities',
actionLabel: 'Open Pulse Pro downloads',
actionUrl: 'https://pulserelay.pro/download.html',
});
});

View file

@ -74,6 +74,11 @@ export interface SelfHostedActivationNoticeCopy {
body: string;
}
export interface LicenseExternalActionCopy {
actionLabel: string;
actionUrl: string;
}
export interface SelfHostedRecoveryPresentation {
disclosureLabel: string;
disclosureDescription: string;
@ -82,6 +87,7 @@ export interface SelfHostedRecoveryPresentation {
helpTextBeforeTerms: string;
helpTextAfterTerms: string;
termsLabel: string;
privateRuntimeNotice: SelfHostedActivationNoticeCopy & LicenseExternalActionCopy;
activateIdleLabel: string;
activatePendingLabel: string;
clearIdleLabel: string;
@ -98,6 +104,7 @@ export interface SelfHostedCurrentPlanPresentation {
includedExtras: string[];
supplementalBadges: string[];
supplementalSummary?: string;
privateRuntimeAction?: LicenseExternalActionCopy;
}
export interface SelfHostedActivationProofItem {
@ -116,6 +123,8 @@ export interface SelfHostedActivationProofPresentation {
export interface SelfHostedActivationSuccessPresentation extends LicenseInlineNotice {
highlightsLabel: string;
highlights: string[];
actionLabel?: string;
actionUrl?: string;
}
export type SelfHostedActivationSuccessSource = 'manual' | 'purchase';
@ -156,6 +165,8 @@ const PRO_RUNTIME_REQUIRED_TIERS = new Set([
'enterprise',
]);
export const PULSE_PRO_DOWNLOAD_URL = 'https://pulserelay.pro/download.html';
const isActivePaidRuntimeState = (subscriptionState?: string | null): boolean => {
const normalized = (subscriptionState || '').trim().toLowerCase();
return normalized === 'active' || normalized === 'grace' || normalized === 'trial';
@ -436,13 +447,13 @@ export const getSelfHostedCurrentPlanPresentation = ({
if (runtimeMismatch) {
supplementalBadges.push('Pro runtime missing');
supplementalDetails.unshift(
'Public GitHub releases and the public Docker image are community builds. Install the private Pulse Pro runtime to test Pro-only runtime hooks during the trial.',
'Public GitHub releases and the public Docker image are community builds. Open Pulse Pro downloads with your activation key to install the private Pulse Pro runtime and test Pro-only runtime hooks during the trial.',
);
}
return {
title: `Current plan: ${planLabel} Trial`,
body: runtimeMismatch
? `${planLabel} trial entitlement is active, but this install is running the community runtime. Install the private Pulse Pro runtime from the paid download page to use Pro-only features.`
? `${planLabel} trial entitlement is active, but this install is running the community runtime. Open Pulse Pro downloads with your activation key and install the private Pulse Pro runtime to use Pro-only features.`
: unlockedFeatures.length > 0
? `${planLabel} trial capabilities are active on this instance right now.`
: `${planLabel} trial entitlement is being confirmed for this instance.`,
@ -452,6 +463,14 @@ export const getSelfHostedCurrentPlanPresentation = ({
includedExtras,
supplementalBadges,
supplementalSummary: supplementalDetails.join(' '),
...(runtimeMismatch
? {
privateRuntimeAction: {
actionLabel: 'Open Pulse Pro downloads',
actionUrl: PULSE_PRO_DOWNLOAD_URL,
},
}
: {}),
};
}
@ -477,13 +496,17 @@ export const getSelfHostedCurrentPlanPresentation = ({
);
return {
title: `Current plan: ${planLabel}`,
body: `${planLabel} is active, but this install is running the community runtime. Install the private Pulse Pro runtime from the paid download page to use Pro-only features such as Audit Log, Audit Webhooks, RBAC, and governed remediation.`,
body: `${planLabel} is active, but this install is running the community runtime. Open Pulse Pro downloads with your activation key and install the private Pulse Pro runtime to use Pro-only features such as Audit Log, Audit Webhooks, RBAC, and governed remediation.`,
unlockedFeaturesLabel,
unlockedFeatures,
includedExtrasLabel: includedExtras.length > 0 ? 'Included extras' : undefined,
includedExtras,
supplementalBadges,
supplementalSummary: supplementalDetails.join(' '),
privateRuntimeAction: {
actionLabel: 'Open Pulse Pro downloads',
actionUrl: PULSE_PRO_DOWNLOAD_URL,
},
};
}
@ -588,7 +611,7 @@ export const getSelfHostedActivationProofPresentation = (
statusLabel: hasProRuntime ? 'Active' : 'Needs attention',
detail: hasProRuntime
? 'This install reports the private Pulse Pro runtime.'
: 'This install reports the community runtime. Install the private Pulse Pro runtime from the paid download page; public GitHub releases and the public Docker image do not include Pro-only runtime hooks.',
: `This install reports the community runtime. Open ${PULSE_PRO_DOWNLOAD_URL} with your activation key and install the private Pulse Pro runtime; public GitHub releases and the public Docker image do not include Pro-only runtime hooks.`,
});
}
items.push(
@ -698,13 +721,19 @@ export const getSelfHostedActivationSuccessPresentation = ({
body:
source === 'purchase'
? runtimeMismatch
? `Checkout completed and the license is active. This install is still running the community runtime, so install the private Pulse Pro runtime from the paid download page before using Pro-only features.`
? `Checkout completed and the license is active. This install is still running the community runtime, so open Pulse Pro downloads with your activation key and install the private Pulse Pro runtime before using Pro-only features.`
: `Checkout completed and this instance is now running ${planLabel}.`
: runtimeMismatch
? `The activation key was accepted. This install is still running the community runtime, so install the private Pulse Pro runtime from the paid download page before using Pro-only features.`
? `The activation key was accepted. This install is still running the community runtime, so open Pulse Pro downloads with your activation key and install the private Pulse Pro runtime before using Pro-only features.`
: `The activation key was accepted and this instance is now running ${planLabel}.`,
highlightsLabel: runtimeMismatch ? 'Licensed capabilities' : 'Available now on this instance',
highlights,
...(runtimeMismatch
? {
actionLabel: 'Open Pulse Pro downloads',
actionUrl: PULSE_PRO_DOWNLOAD_URL,
}
: {}),
};
};
@ -886,6 +915,12 @@ export const SELF_HOSTED_RECOVERY_PRESENTATION: SelfHostedRecoveryPresentation =
'Paste the Pulse v6 activation key shown on the hosted checkout success page. A backup copy is also sent by email, but the hosted success page is the primary handoff. You can also paste a legacy Pulse v5 Pro/Lifetime license key and Pulse will exchange it automatically during activation when migration is available. By activating a license, you agree to the',
helpTextAfterTerms: '.',
termsLabel: 'Terms of Service',
privateRuntimeNotice: {
title: 'Paid Docker and Linux installs use a private runtime',
body: 'Public GitHub releases and the public Docker image are community builds. They can accept an activation key, but Pro-only runtime hooks require the private Pulse Pro Docker image or Linux archive.',
actionLabel: 'Open Pulse Pro downloads',
actionUrl: PULSE_PRO_DOWNLOAD_URL,
},
activateIdleLabel: 'Activate Key',
activatePendingLabel: 'Activating...',
clearIdleLabel: 'Clear Key',

View file

@ -216,6 +216,12 @@ helm upgrade --install pulse oci://ghcr.io/rcourtman/pulse-chart --version ${VER
See the [Installation Guide](https://github.com/rcourtman/Pulse#installation) for details.
Paid Pulse Pro, Relay, and eligible legacy customers: public GitHub release
assets and the public \`rcourtman/pulse\` Docker image are community builds. They do
not include the private Pulse Pro runtime hooks. Use
https://pulserelay.pro/download.html with your activation key to get the
private Pulse Pro Docker image or Linux archive.
GUIDELINES:
- Write plain, factual release notes. No marketing language or excitement.
- Only mention features that exist in the FINAL code state

View file

@ -349,13 +349,14 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("It does not automatically check out or build `pulse-enterprise`.", runbook)
self.assertIn("public `pulse-v...` release archives are OSS runtime artifacts", runbook)
self.assertIn("`pulse-pro-v...` archives", runbook)
self.assertIn("`bin/pulse --version` identifies `Pulse Pro (Enterprise)`", runbook)
self.assertIn("`bin/pulse --version` identifies `Pulse Pro`", runbook)
self.assertIn("Paid-user GA is part of that same release boundary", contract)
self.assertIn(
"the public Pulse release workflow builds OSS `pulse-v...` artifacts only",
normalize_ws(contract),
)
self.assertIn("`pulse-pro-v...` archives identify `Pulse Pro (Enterprise)`", contract)
self.assertIn("`pulse-pro-v...` archives identify `Pulse Pro`", contract)
self.assertIn("https://pulserelay.pro/download.html", contract)
self.assertIn("control_plane.py --branch-for-version", content)
self.assertIn('git fetch --prune origin main "${REQUIRED_BRANCH}" --tags', content)
self.assertIn('REQUIRED_BRANCH: ${{ steps.branch_policy.outputs.required_branch }}', content)

View file

@ -80,6 +80,8 @@ def build_installation_section(version: str) -> str:
f"Update your `docker-compose.yml` to use `rcourtman/pulse:{version}`",
"",
"See the [Installation Guide](https://github.com/rcourtman/Pulse#installation) for complete setup instructions.",
"",
"Paid Pulse Pro, Relay, and eligible legacy customers: public GitHub release assets and the public `rcourtman/pulse` Docker image are community builds. They do not include the private Pulse Pro runtime hooks. Use https://pulserelay.pro/download.html with your activation key to get the private Pulse Pro Docker image or Linux archive.",
]
)

View file

@ -87,6 +87,11 @@ Old metadata section.
self.assertEqual(body.count("## Installation"), 1)
self.assertEqual(body.count("## Promotion Metadata"), 1)
self.assertIn("docker pull rcourtman/pulse:6.0.0-rc.2", body)
self.assertIn(
"public GitHub release assets and the public `rcourtman/pulse` Docker image are community builds",
body,
)
self.assertIn("https://pulserelay.pro/download.html", body)
self.assertIn("- Rollback target: v5.1.28", body)
def test_current_release_packets_use_pulse_mobile_handoff_copy(self) -> None: