TierBusiness: Pro's feature set, 365-day history retention, uncapped core monitoring like every self-hosted tier, Business display name, and a slot in the min-tier ordering. Differentiation is by max_users limit, retention, and support rather than features (multi-user is already a Pro capability via RBAC; see specs/pricing-segmentation.md binder findings). Dormant until the license server issues business plan versions; no checkout, pricing, or UI surface references it yet. Contract shape recorded as cloud-paid Extension Point 26 and pinned by TestBusinessTierContractShape.
199 KiB
Cloud Paid Contract
Contract Metadata
{
"subsystem_id": "cloud-paid",
"lane": "L3",
"contract_file": "docs/release-control/v6/internal/subsystems/cloud-paid.md",
"status_file": "docs/release-control/v6/internal/status.json",
"registry_file": "docs/release-control/v6/internal/subsystems/registry.json",
"dependency_subsystem_ids": []
}
Purpose
Own cloud plan/version semantics, entitlement limits, hosted billing/runtime agreement, the Pulse Cloud control plane, provider-hosted MSP account bootstrap/licensing, hosted tenant lifecycle, and cloud-specific enforcement rules.
Pulse Pro commercial Pulse Intelligence value reporting is a cloud-paid proof
surface. It must compare full-loop, approved-execution-loop, Assistant-loop,
Assistant-resolved-loop, external-agent-loop, external-agent-resolved-loop,
Patrol-resolution, resolved-operations-loop, Patrol-guided resolved-loop,
Patrol-control completed-loop, Patrol-control resolved-loop, paid Patrol-control
completed-loop, paid Patrol-control resolved-loop, legacy Pro activation
compatibility mirrors, MCP adapter
operations-loop, MCP adapter approved-execution-loop, MCP adapter
resolved-operations-loop, and
MCP/external-agent capability-class cohorts against the generic Assistant chat
baseline with activation, retention, paid-rate, free-start conversion, and
signal-to-paid deltas.
The Patrol-guided resolved-loop cohort is the first-party completion signal:
it requires the pulse_patrol operations-loop starter marker plus resolved
operations-loop proof, so Patrol starter interest is not mistaken for a
completed activation loop.
The Patrol-control completed-loop cohort is a first-class telemetry signal for
historical conversion and retention analysis. It requires the Patrol control
starter or its legacy Pro activation entry-point alias, Patrol issue evidence,
contextual Assistant or external-agent collaboration, and either a rejected
governed decision or an approved governed decision with verified outcome proof,
so a deliberate operator rejection can complete the decision loop without being
misreported as an approved-and-resolved outcome. Local self-hosted Pro
operations-status presentation must apply the same distinction through the
shared operations-loop patrolControlValueState, falling back to
patrolAutonomyValueState and then proActivationValueProofState only for
compatibility: governed_decision_recorded remains partial decision status
with a continue path, legacy verified_needs_mcp is treated as verified
first-party Patrol value with MCP readiness left as optional external-agent
context, and verified may be presented as a verified Patrol operations
outcome or suppress a new starter marker. Paid-plan status rows must describe
missing or partial Pro action entitlements as Patrol investigation/remediation
capability, not as generic Pro operations capability, so the commercial
surface tells users which governed Patrol behavior is affected.
The Patrol-control resolved-loop cohort is a first-class telemetry signal for
paid Patrol control value, not a report-time intersection of generic starter
interest and generic resolved-loop proof. Pulse emits it only when a Patrol
control or legacy Pro activation starter, Patrol issue evidence, contextual
Assistant or external-agent collaboration, an approved governed decision, and
verified outcome proof are present, and the license-server value report must
consume that explicit primary flag.
The paid Patrol-control completed-loop and resolved-loop cohorts are also
first-class telemetry signals. Pulse emits them only when the current coarse
paid_license posture coexists with the corresponding Patrol-control
completed-loop or resolved-loop proof in the same telemetry payload. Legacy Pro
activation completed/resolved and paid cohort fields may mirror those primary
Patrol-control values for continuity. The value report must use those explicit
booleans for paid-rate, retention, and free-to-paid comparisons, and must not
infer paid Patrol-control value from exact tiers, account links, checkout
sessions, license IDs, or a report-time join against customer identity.
Approved-execution-loop
metrics mean Patrol issue evidence plus Assistant governed-context, direct
external-agent, or MCP collaboration plus at least one approved governed-action
attempt; complete-loop metrics mean Patrol issue evidence plus contextual collaboration plus an
approved or rejected governed-action decision signal. Those metrics must remain
separate from broader governed-action activity so action plans and approval
requests are not mistaken for completed operations-loop proof. Capability-class signal-to-paid
metrics must use the first time that specific capability was observed while
the install was still free, then count conversion only when paid activation
happened after that capability signal; paid installs that first use a
capability after purchase must not be counted as capability-driven conversion
proof.
Provider-hosted MSP bootstrap output is part of the cloud-paid operator
contract. It must carry the resolved MSP plan version, plan source, workspace
limit, and validated provider MSP license id/email so installability proof can
distinguish signed license-backed activation from local environment fallback
configuration.
Provider-hosted MSP status uses the same tenant registry, health summary, and
stuck-provisioning threshold as the control-plane cleanup loop, so operator
readiness reports and automated failure handling cannot drift into separate
definitions of a failed provider workspace.
Provider-hosted MSP recovery uses the same license-backed provider identity,
workspace registry, tenant data, and canonical tenant-runtime rollout path as
the rest of the control plane so failed, stuck, or unhealthy client workspaces
are repaired without introducing a second provisioning model.
Provider-hosted MSP backup, verification, and restore are part of the cloud-paid
operator contract because the recovery artifact must preserve the provider's
license-backed plan identity, control-plane account/workspace registry, and
tenant-local runtime state without depending on Stripe billing surfaces.
Provider-hosted MSP tenant runtime isolation is part of the cloud-paid runtime
contract. CP_DOCKER_NETWORK names the provider ingress and support network,
not the client runtime boundary: each client workspace runtime must be created
on a derived per-tenant Docker bridge, and support containers must be explicitly
attached to that bridge before the tenant runtime is started.
Provider-hosted MSP control-plane host access is part of the same runtime
contract. The internet-facing control plane must not receive broad host-root or
Docker-data read mounts for storage admission checks; it may stat only narrow
operator-owned marker directories on the target filesystems. Docker daemon
access must be routed through the packaged socket proxy rather than a raw
read-write socket mount, and audit/rate-limit client IP recovery must honor
forwarded headers only from configured trusted provider proxy CIDRs.
Provider-hosted MSP report branding is a tenant runtime configuration
contract, not control-plane report generation. The control plane may accept
provider-default report brand environment values and pass them into each tenant
container as generic PULSE_REPORT_PROVIDER_BRAND_* runtime configuration, but
the tenant Pulse runtime owns report rendering, per-workspace override loading,
scheduled report cadence/delivery, generated output retention, and the
white_label entitlement gate. This keeps provider-hosted MSP Stripe-free and
avoids a cloud-control-plane report data path across clients.
Canonical Files
pkg/licensing/features.gopkg/licensing/billing_state_normalization.gopkg/licensing/database_source.gopkg/licensing/evaluator.gopkg/licensing/models.gopkg/licensing/activation_types.gopkg/licensing/token_source.gopkg/licensing/entitlement_payload.gopkg/licensing/hosted_subscription.gopkg/licensing/dev_mode_features.gopkg/licensing/service.gopkg/licensing/grant_refresh.gopkg/licensing/revocation_poll.gopkg/licensing/license_server_client.gopkg/licensing/persistence.gopkg/licensing/activation_store.gopkg/licensing/trial_activation.gopkg/licensing/purchase_return.gopkg/licensing/stripe_subscription.gopkg/licensing/monitored_system_limit.gointernal/cloudcp/account/audit.gointernal/cloudcp/account/handlers.gointernal/cloudcp/account/tenant_handlers.gointernal/cloudcp/auth/handlers.gointernal/cloudcp/auth/session.gointernal/cloudcp/config.gointernal/cloudcp/entitlements/service.gointernal/cloudcp/portal/handlers.gointernal/cloudcp/portal/page.go29a.internal/cloudcp/portal/setup_facts.gointernal/cloudcp/public_cloud_signup_handlers.gointernal/cloudcp/registry/models.gointernal/cloudcp/registry/registry.gointernal/cloudcp/routes.gointernal/cloudcp/stripe/provisioner.gointernal/hosted/provisioner.gofrontend-modern/src/App.tsxfrontend-modern/src/AppLayout.tsx37a.frontend-modern/src/components/CommercialMigrationBanner.tsxfrontend-modern/src/useAppRuntimeState.tsfrontend-modern/src/components/Settings/BillingAdminPanel.tsxfrontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsxfrontend-modern/src/components/Settings/OrganizationBillingPanel.tsxfrontend-modern/src/components/Settings/OrganizationBillingLoadingState.tsxfrontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsxfrontend-modern/src/components/Settings/ProLicensePanel.tsxfrontend-modern/src/components/Settings/ProLicensePlanSection.tsxfrontend-modern/src/components/Settings/CommercialBillingSections.tsxfrontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsxfrontend-modern/src/components/Settings/RelaySettingsPanel.tsxfrontend-modern/src/components/Settings/RelayPairingSection.tsxfrontend-modern/src/components/Settings/useBillingAdminPanelState.tsfrontend-modern/src/components/Settings/useOrganizationBillingPanelState.tsfrontend-modern/src/components/Settings/useProLicensePanelState.tsfrontend-modern/src/components/Settings/useRelaySettingsPanelState.tsfrontend-modern/src/pages/PricingHandoff.tsxfrontend-modern/src/utils/apiClient.tsfrontend-modern/src/utils/cloudPlans.tsfrontend-modern/src/utils/commercialBillingModel.tsfrontend-modern/src/utils/licensePresentation.tsfrontend-modern/src/utils/monitoredSystemPresentation.tsfrontend-modern/src/utils/pricingHandoff.tsfrontend-modern/src/utils/selfHostedPlans.tsfrontend-modern/src/utils/upgradePresentation.tsfrontend-modern/src/components/Settings/selfHostedBillingPresentation.tspkg/licensing/trial_activation_public_key_override_dev.gopkg/licensing/trial_activation_public_key_override_release.gopkg/licensing/testing_helpers.gopkg/licensing/self_hosted_feature_catalog.gofrontend-modern/src/utils/selfHostedFeatureCatalog.generated.tsinternal/cloudcp/server.go,internal/cloudcp/authz.go,internal/cloudcp/commercial_identity.go,internal/cloudcp/security.gointernal/cloudcp/health_monitor.go,internal/cloudcp/health_stuck_provisioning.go,internal/cloudcp/tenant_state_metrics.go,internal/cloudcp/ratelimit.go81a.internal/cloudcp/proxytrust/client_ip.gointernal/cloudcp/hosted_entitlement_handlers.go,internal/cloudcp/url_helpers.gointernal/cloudcp/admin/handlers.go,internal/cloudcp/admin/status.go,internal/cloudcp/auditlog/auditlog.gointernal/cloudcp/cpmetrics/metrics.go,internal/cloudcp/cpsec/nonce.go,internal/cloudcp/static_assets.go,internal/cloudcp/favicon.svginternal/cloudcp/email/sender.go,internal/cloudcp/email/templates.gointernal/cloudcp/handoff/handler.go,internal/cloudcp/handoff/handoff.gointernal/cloudcp/stripe/grace_enforcer.go,internal/cloudcp/stripe/helpers.go,internal/cloudcp/stripe/reconciler.go,internal/cloudcp/stripe/webhook.gointernal/hosted/hosted_metrics.go,internal/hosted/reaper.gointernal/cloudcp/public_msp_signup_handlers.gointernal/cloudcp/provider_msp_bootstrap.gointernal/cloudcp/provider_msp_backup.gointernal/cloudcp/provider_msp_recovery.go
Shared Boundaries
frontend-modern/src/components/CommercialMigrationBanner.tsxshared withfrontend-primitives: the global commercial migration notice is both a cloud-paid entitlement recovery surface and a shared app-shell notice primitive consumer. The banner's commercial migration predicates, copy, retry polling, dismissal state, and billing-plan destination remain cloud-paid owned, while the visible banner shell, icon, action slot, and dismiss chrome must compose the sharedInlineNoticeprimitive instead of carrying local paid-state tone classes, raw SVGs, or page-local buttons. Pulse Mobile pairing insidefrontend-modern/src/components/Settings/useRelaySettingsPanelState.tsis also a commercial relay surface only for entitlement and operator presentation. When the backend returns409 onboarding_not_ready, the settings surface must preserve API-owned diagnostics and clean up the temporary server-minted mobile token; it must not downgrade that response into a generic paid-state prompt or create browser-owned relay credentials.frontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsxshared withagent-lifecycle: the monitored-system impact preview is both a platform-connections lifecycle surface and a canonical cloud-paid monitored-system presentation boundary.frontend-modern/src/useAppRuntimeState.tsshared withperformance-and-scalability: the authenticated app runtime bootstrap is both a hosted commercial org-context boundary and a protected app-shell performance boundary. The app runtime may usessoSessionDisplayNamefrom security status for visible signed-in chrome, but hosted/commercial organization context must remain bound to the stable authenticated principal carried separately inssoSessionUsernameand backend request context. Display labels, contact emails, and SSO name claims must not become hosted owner/member identity, billing authority, entitlement state, or organization bootstrap source of truth.internal/api/licensing_bridge.goshared withapi-contracts: commercial licensing bridge handlers carry both API payload contract and cloud-paid entitlement boundary ownership.internal/api/licensing_handlers.goshared withapi-contracts: commercial licensing handlers carry both API payload contract and cloud-paid entitlement boundary ownership. That same shared licensing boundary also owns installation-version and runtime-build continuity for authenticated v6 installs:internal/api/router.goandinternal/api/licensing_handlers.gomust hand the canonical process version and runtime identity intopkg/licensing/service.goandpkg/licensing/grant_refresh.go, and cloud-paid transport must send those values on activate, legacy exchange, and grant refresh instead of inferring install version or paid-runtime status from browser state, dev build metadata, public image tags, or outbound usage telemetry. Active self-hosted paid entitlements must also treat runtime identity as a first-class product state. A paid Pro, Pro Annual, Pro+, lifetime, or enterprise entitlement on a non-Pro or missing runtime identity must render the private Pulse Pro runtime handoff in the local Plans surface rather than presenting as fully healthy. License-server support/admin payloads must preserve the raw runtime build and expose a normalizedpro,community, orunknownstatus so paid-runtime support triage does not depend on interpreting Docker tags, public release names, or customer screenshots. That same shared licensing boundary also owns paid-migration degradation visibility and recovery. A persisted v5 license that exists but cannot be read or decrypted must publish a terminalcommercial_migrationstate (persisted_license_unreadable) instead of downgrading to Community behind a log line, and startup legacy-exchange failures classified as pending must self-retry in the background with backoff for the life of the process so a transient license-server or DNS failure at first boot never strands a paying upgrader on Community until a manual restart. Signature-valid v5 JWTs that are expired beyond the v5 grace window but correspond to a newer retrievable server-side key or live entitlement must classify as terminal stale-key recovery (exchange_stale_keywithretrieve_current_key) rather than generic invalid-key failure; malformed, signature-invalid, and truly lapsed keys must keep the generic terminal rejection path. Transport-level legacy-exchange failures must preserve the first continuous failure timestamp oncommercial_migration.first_failed_atand, after 24 hours, keep retrying while surfacingexchange_connectivity_requiredwith the outboundlicense.pulserelay.proconnectivity policy instead of rendering ordinary pending copy forever.internal/api/licensing_legacy_retry.goshared withapi-contracts: the background legacy-exchange retry loop carries both API payload contract and cloud-paid entitlement boundary ownership.internal/api/payments_webhook_handlers.goshared withapi-contracts: commercial payment webhook handlers carry both API payload contract and cloud-paid billing boundary ownership.internal/api/public_signup_handlers.goshared withapi-contracts: hosted signup handlers carry both API payload contract and cloud-paid hosted provisioning boundary ownership. That shared monitored-system presentation boundary also owns disabled provider-connection copy. Commercial entitlement surfaces must treat canonical zero-delta and removal-only TrueNAS or VMware previews as non-consuming or capacity-freeing changes rather than warning users that a disabled connection still grows monitored-system usage. VMware vSphere network inventory is product navigation and resource context, not a separate commercial unit. The/vmware/networksroute may display API-native network rows and those rows may contribute to connection previews, but cloud-paid surfaces must continue to meter only the governed monitored-system grouping result. Network child-resource volume must not become a hosted usage cap, upgrade prompt, or billing-admission condition. That same shared signup boundary also owns the public privacy floor: syntactically valid/api/public/signuprequests resolve to one uniform202 AcceptedPulse Account response whether provisioning/email side effects ran or were suppressed by owner-email throttling.internal/cloudcp/auth/magiclink.goshared withsecurity-privacy: control-plane magic-link HMAC handling is both a Pulse Cloud account-access boundary and a security/privacy token-secrecy boundary.internal/cloudcp/auth/magiclink_store.goshared withsecurity-privacy: control-plane magic-link persistence is both a Pulse Cloud account-access boundary and a security/privacy storage-hardening boundary.internal/cloudcp/docker/labels.goshared withdeployment-installability: hosted tenant Docker labels are both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary.internal/cloudcp/docker/manager.goshared withdeployment-installability: hosted tenant container management is both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary. Hosted tenant container creation must also bound Dockerjson-filelogs through the control-plane Docker manager so tenant runtime logging cannot fill the live Pulse Cloud host independently of tenant data quotas. Hosted tenant container creation must also stamp tenant identity into the runtime environment: the Docker manager resolves the workspace display name from the tenant registry at container-create time and injectsPULSE_TENANT_NAMEalongsidePULSE_TENANT_ID, so alert webhook payloads from hosted client runtimes carry a human-readable workspace label instead of falling back to the tenant ID. Display-name changes after creation apply on the next tenant runtime rollout, which recreates the container with freshly resolved environment. Provider-hosted MSP and Pulse-hosted tenant creation must also prepare or fail closed on the configured tenant runtime image before mutating the workspace into a state that expects a live runtime container; readiness checks must surface missing Docker daemon, network, image, and storage prerequisites as operator-facing failures. Hosted checkout and MSP workspace provisioning must also pass the control-plane storage admission guard before tenant/account mutation: root filesystem, tenant data, Docker runtime store, and Docker build-cache thresholds are part of the Cloud paid readiness contract rather than an operator-only cleanup script. The same cloud audit contract must fail on stale proof/canary account rows and paid hosted entitlements whose tenant rows are missing, because either residue can recreate or mask hosted runtime state after a cleanup. Disposable proof-account cleanup must use the control-plane registry as the source of truth rather than ad hoc SQL: account deletion must refuse accounts that still own tenant rows, remove account-owned membership, invitation, and Stripe account metadata in one registry transaction, and leave user identity rows intact unless a separately governed identity-retention rule says otherwise. The live production host must run that cloud audit through the private Pulse Pro operations bundle on a recurring systemd timer, write durable status/log output, and emit Prometheus textfile metrics so a clean GA baseline is continuously monitored rather than manually rediscovered. Provider-hosted MSP runtime isolation must keep client tenant runtimes off the shared provider ingress/support network. The Docker manager must derive a per-client tenant bridge, label it with tenant identity, pin Traefik routing to that tenant bridge, attach only provider support containers selected by provider MSP labels, start the tenant runtime only after those support attachments succeed, prefer the tenant bridge for health checks, and remove tenant bridges when the owning runtime is removed. Tenant runtime containers must also start with the provider-hosted escape hardening defaults owned by the Docker manager: no-new-privileges, dropped Linux capabilities unless explicitly reintroduced by a governed need, Docker's default seccomp profile still active, a read-only root filesystem, and bounded writable tmpfs mounts only for runtime scratch paths. These defaults are part of the client isolation contract, not optional compose decoration. Tenant runtime containers must also start as the rootless Pulse runtime user instead of entering the image's root-owned chown andsu-execbranch. The Docker manager owns host-side preparation of the tenant data directory, including recursive ownership alignment to the configured tenant UID/GID and strict permissions on tenant key files, soCapDrop: ["ALL"]and read-only root filesystems remain compatible with first workspace startup. Client-bound proof must cover the boundary itself: workspace limits must not be raceable past the licensed cap, handoff tokens must not be replayed or retargeted across workspaces, org-bound agent install/report tokens must not write into another client runtime, and rotated-out install tokens must fail immediately. Provider-default report branding is part of the same tenant environment contract:internal/cloudcp/docker/manager.gomay injectPULSE_REPORT_PROVIDER_BRAND_DISPLAY_NAME, logo path/data, and logo format into each tenant container, but it must not collect report data or render PDFs in the control plane. Tenant-local reporting, tenant-local schedules, and tenant-local licensing decide whether that configured brand appears and how recurring report delivery runs.pulse_hosted_mspis the Pulse-operated form of the same Stripe-free MSP control-plane family, not the public Pulse-hosted SaaS checkout path. It must share the license-backed MSP plan source, workspace limit policy, disabled public signup and Stripe billing routes, isolated tenant runtime networks, tenant-local report branding, and provider MSP operator commands withprovider_hosted_mspwhile preserving its own control-plane mode value for status, backup manifests, and operational audit.internal/cloudcp/provider_msp_backup.goshared withdeployment-installability: provider-hosted MSP backup is both a cloud-paid license/account/runtime continuity boundary and a deployment-installability recovery artifact boundary. License-backed provider MSP backups must include the signed MSP license file as a recovery artifact while exposing only license metadata in command output, restore must recover that license as an explicit operator artifact, and the archive must stay Stripe-free so provider-hosted MSP recovery does not inherit Pulse-hosted SaaS billing assumptions.internal/cloudcp/provider_msp_recovery.goshared withdeployment-installability: provider-hosted MSP failed-workspace recovery is both a cloud-paid license/account/runtime continuity boundary and a deployment-installability recovery artifact boundary. Provider-hosted MSP recovery must require the signed provider MSP license source by default, preserve the client workspace boundary, refuse to start from empty tenant data, and mark a workspace active only after the canonical tenant-runtime rollout path has produced a healthy runtime.internal/cloudcp/tenant_runtime_rollout.goshared withdeployment-installability: hosted tenant runtime rollout is both a Pulse Cloud runtime contract boundary and a deployment-installability release-rollout boundary. Hosted tenant runtime reconciliation must treat a registered tenant with preserved tenant data but no live Docker runtime as a recoverable managed state, not as a terminal skip. The control-plane-owned reconcile path must recreate the canonical tenant container, health-check it, and persist the new runtime identity before hosted billing/auth surfaces are considered coherent. Tenant runtime rollout and missing-runtime restore must fail closed on the same storage admission guard before snapshotting or swapping containers. Provider-hosted MSP rollout and recovery must preserve the same isolated tenant network contract when recreating, reconciling, or rolling tenant containers; a recovered client workspace must not fall back to the shared provider ingress/support network as its runtime network.tenant-runtime rollout --all --image <target>is the canonical hosted fleet image upgrade path for active tenant runtimes. Its--dry-runmode must print the target-image rollout plan before mutation,--allmust select active tenants only, and apply must still use the canonical per-tenant snapshot, health-check, and rollback path.tenant-runtime reconcile --allremains contract/routing drift repair for each tenant's current image line, not an image-line upgrade path.
The real pulse-pro license-server legacy checkout issuance, recurring
renewals, manual issue, and legacy exchange flows are part of that same
cloud-paid continuity boundary as well. Those flows must resolve grandfathered
recurring continuity from explicit active pre-cutover state, not from bare
legacy plan IDs, stale historical tokens, or issue timestamps alone.
The canonical operator preview for that cutover now lives in
pulse-pro/scripts/grandfathered_recurring_cutover_preview.py, and it must use
the same active-at-snapshot rule as the live server before anyone sets
PULSE_LICENSE_GRANDFATHERED_RECURRING_SNAPSHOT_AT.
The retired server-side checkout funnel analytics, alerting, digest
generation, and admin summary payloads are no longer part of the active v6
proof floor. pulse-pro:license-server/v6_checkout.go may accept legacy
funnel-telemetry fields only as ignored compatibility input for older static
site deploys; active paid-feature proof must stay on checkout creation,
portal-handoff state, pricing/catalog truth, download delivery, and license or
relay entitlement behavior rather than requiring the removed funnel monitoring
files.
Pulse Intelligence paid-value evidence is a separate private admin aggregate,
not a revival of those retired funnel stores. The pulse-pro license server
may expose /v1/admin/pulse-intelligence/value and the matching admin-console
strip only as coarse install cohorts over outbound usage telemetry pings: paid
install rate, free-to-paid conversion, returning-install retention, and whether
the full Patrol -> contextual collaboration -> governed action loop, the
stricter Patrol -> contextual collaboration -> approved execution loop, and
the approved-action-success and adapter-specific Pulse MCP loop variants are
active, including the stricter resolved operations loop split by first-party
Assistant collaboration, external-agent collaboration, and Pulse MCP adapter
use.
The same aggregate may expose an operations_funnel object that keeps the
loop proof readable as a staged anonymous cohort sequence: loop configured,
Patrol activity, Assistant or external-agent collaboration, governed action,
approved execution, approved action success, Patrol resolution, resolved
operations loop, Assistant resolved loop, external-agent resolved loop, active
30-day use, retained installs, Patrol-control completed-loop and resolved-loop
proof, paid Patrol-control cohorts, and full-loop free-to-paid conversion compared with generic
Assistant-only chat. That funnel is a projection over the same coarse telemetry
counters and value cohorts; it must not introduce per-install drilldowns or raw
commercial account links. Source-specific guided starter cohorts may split the
same anonymous operations-loop starter count into Assistant, Patrol, primary
Patrol-control, legacy Pro activation entry-point, and Pulse MCP sources so
conversion reporting can tell which entry point led to loop entry, but those cohorts
remain starter evidence only and must not be counted as completed
operations-loop, approved execution, or resolved-loop proof.
Patrol-control completed-loop and resolved-loop funnel stages must come from
the explicit content-free Patrol-control booleans; resolved proof may satisfy
completed proof, but a rejected terminal completed loop must never be counted as
resolved proof. Legacy Pro activation completed/resolved booleans may be
retained only as mirrors for historical cohort continuity.
The self-hosted Pro plan surface may consume the authenticated, content-free
GET /api/agent/operations-loop/status projection only to choose the primary
Patrol handoff in the current-plan summary: set Patrol control when no work is
active, continue unfinished Patrol work, or review a pending governed decision.
The plan capability details disclosure must stay entitlement/runtime
diagnostics only. It must not add a Patrol work, started,
decision-recorded, verified, external-agent readiness, or compatibility
operations-loop status row from Patrol starter/completed/resolved counts,
legacy proActivation* aliases, legacy patrolAutonomy* mirrors, shared
status nextAction, or shared status progressLabel; those fields are
Patrol-page context and telemetry, not paid-plan detail copy. The plan surface
must not expose prompts, accounts, resource names, finding IDs, action IDs,
commands, token metadata, or any per-install drilldown, and lack of that
projection must not block entitlement or plan rendering. The visible Pro plan
card must keep the paid user's first-party task clear: choose Patrol mode and
let Patrol handle infrastructure work. The first-party commercial copy owns
the product framing: stale activation-loop, operations-loop, or MCP setup
progress text from the projection must not become the visible paid CTA when
the correct next step is Patrol control or Patrol work.
Commercial, plan, and activation-success copy must use Choose Patrol mode
for the paid operator boundary and frame it as setting how autonomous Patrol
should be. Self-hosted Community copy should state the features available on
that install, such as watch-only Patrol, instead of explaining unavailable
fix classes. Pro plan cards may describe hands-on Patrol modes and verified
fixes, but they must not revive activation-loop, MCP readiness, or internal
proof vocabulary as the paid user's default setup story; route-backed anchors,
telemetry, and compatibility wire fields may retain stable names such as
patrol_control and patrolControl*.
Routine self-hosted Plans and capability-status copy must describe what is
available on the instance and route recovery as refresh the plan or open recovery; it must not describe normal setup as activation or expose raw
entitlement-payload wording to the operator.
That external-agent readiness boolean means the manifest-owned Pulse MCP
operations-loop contract is available and one non-expired API token covers the
full published operations-loop scope set; a partial MCP token is not paid value
proof, missing MCP readiness must not downgrade verified Patrol value, and it
must not become an activation/setup step on the default plan surface.
The completed-loop count consumed here is the API-owned terminal-loop proof: it
requires Patrol control starter evidence, Patrol issue evidence, contextual
Assistant or external-agent collaboration, and either a rejected governed
decision or an approved governed decision with verified outcome proof.
The resolved-loop count consumed here is already the API-owned full-loop proof:
it requires Patrol control starter evidence, Patrol issue evidence, contextual
Assistant or external-agent collaboration, an approved governed decision, and a
verified outcome in the same evidence window.
The steady-state current-plan action may use that same first-party Patrol
control projection to label the Patrol handoff as start, continue, or review,
but it must still route to the Patrol-owned Patrol control anchor rather than
creating a separate commercial activation state machine. Legacy Pro activation
evidence alone must leave the action as a fresh Patrol control handoff.
Start and continue paid Patrol control handoffs from
frontend-modern/src/utils/licensePresentation.ts must compose the
route-backed Patrol control URL from
frontend-modern/src/routing/resourceLinks.ts; the settings panel must not
write the starter marker from a local click handler before navigation, and its
presentation model must not carry a recordActivity switch for that handoff.
Patrol owns consuming that route flag and writing the content-free
patrol_control marker; patrol_autonomy and pulse_pro_activation remain
accepted only as legacy route/activity aliases. Once the action has become verified review,
licensePresentation.ts must route to the plain Patrol control anchor and
keep marker recording disabled.
When that action has become a completed-loop review, it must not write another
Patrol control starter marker; starter telemetry remains for start/continue
entry into the guided journey rather than for post-completion review clicks.
The value report may also expose a value_driver_evidence summary derived only
from those same anonymous cohort deltas. That summary may state whether the
available evidence is insufficient, positive on paid conversion, positive on
activation or retention, or has no positive delta against the generic
Assistant-only baseline; it must not claim causality, store per-install
drilldowns, or introduce a second analytics data source outside telemetry
pings.
That report must not expose install IDs, customer emails, prompts, finding IDs,
resource IDs, or action payloads, and generic Assistant-only chat must remain a
separate cohort so it cannot be counted as proof that the proactive operations
loop is driving paid value.
That same public-pricing boundary also owns the machine-readable v6 pricing
validator and launch checklist in pulse-pro:scripts/validate_public_pricing_model.py
and pulse-pro:V6_LAUNCH_CHECKLIST.md. Those operator surfaces must describe
self-hosted Pulse as core monitoring included by default, must not reintroduce
monitored-system upsell language, and must verify the no-cap release posture
rather than legacy Community limit enforcement.
Self-hosted paid-feature claims must also remain covered by the executable
cross-repo proof bundle in scripts/release_control/paid_feature_claims_proof.py.
When Community, Relay, or Pro claims change, that proof must continue to verify
the public customer-facing copy, canonical licensing matrix, runtime history
entitlement enforcement, frontend plan presentation, public
pricing/checkout/download behavior, Pro admin-extra runtime behavior, and
Relay entitlement acceptance together rather than treating copy, checkout, and
runtime gating as separate unlinked claims.
Uncapped self-hosted plans must also remain uncapped before and after legacy
activation continuity is applied. A captured monitored-system continuity floor
may preserve capacity for capped grants, but it must not be written back as a
raw monitored-system cap for Community, Relay, Pro, Pro+, annual Pro, lifetime,
or other self-hosted uncapped continuity plans.
Extension Points
- Add or change limits through
pkg/licensing/ - Add or change hosted entitlement issuance through
internal/cloudcp/entitlements/service.goHosted entitlement refresh is scoped to active workspace rows only. A tenant in any non-activelifecycle state must return an inactive hosted entitlement result even when the owning account's Stripe subscription is still active, so soft-deleted or suspended workspaces cannot keep refreshing runtime leases. Hosted entitlement refresh must parsestripe_accounts.subscription_stateas the normalized stored vocabulary (active,trial,past_due,canceled, and migration-tolerated terminal states), not as raw Stripe API statuses such astrialing. Raw Stripe status mapping remains limited to webhook/provisioner ingress; stored-row reads must use the licensing-owned stored-state parser so active trials do not fail closed during lease refresh. - Add or change control-plane plan storage through
internal/cloudcp/registry/models.goandinternal/cloudcp/registry/registry.go - Add or change MSP account-scoped workspace provisioning entry handlers through
internal/cloudcp/account/tenant_handlers.go - Add or change public cloud self-serve signup price configuration,
tenant-runtime capacity/log retention configuration, or checkout gating
through
internal/cloudcp/config.goandinternal/cloudcp/public_cloud_signup_handlers.goPublic MSP signup is the Pulse-operated account front door for the same boundary and lives ininternal/cloudcp/public_msp_signup_handlers.go. Starter is the only tier that may expose checkout when the MSP rollout gate is deliberately opened; Growth, Scale, and Enterprise remain request-based even when their Stripe prices are configured for assisted/operator-controlled workflows. MSP price configuration (CP_MSP_STARTER_PRICE_ID,CP_MSP_GROWTH_PRICE_ID,CP_MSP_SCALE_PRICE_ID) is still validated ininternal/cloudcp/config.goagainst the canonicalmsp_starter,msp_growth, andmsp_scaleplan versions. The canonical MSP product model is one provider account managing many isolated client workspaces. A client workspace is the hard monitoring boundary: agent credentials, ingest, dashboards, alert routes, reports, users, audit state, and tenant-local runtime data belong to the client workspace, not to a shared provider monitoring namespace. The MSP account portal is the provider control plane and handoff surface; it must not become a cross-client monitoring console or mint tenant-local agent credentials. Public MSP pricing covers the isolated-client MSP product model, not a promise that a provider-hosted stack is already self-serve. Provider-hosted MSP is the intended default launch direction because the MSP runs the control-plane/runtime stack in their own cloud or infrastructure, but it is gated on a packaged provider artifact that a technical MSP can actually operate. That artifact must deliberately remove the SaaS billing/public signup plane and keep only the provider essentials: client-workspace roster, per-client runtime provisioning, account-to-workspace handoff, provider access management, and workspace-bound install tokens. Pulse-hosted MSP is the same isolated-client workspace model operated by Pulse, but it is not part of the default public launch motion and must remain request-only until tenant isolation, ingest, reporting, operations, and support load are proven. The provider-hosted implementation mode isCP_CONTROL_PLANE_MODE=provider_hosted_msp. In that mode the control plane is Stripe-free by construction: it must not register Stripe webhook or portal billing routes, must reject Stripe API keys, Stripe webhook secrets, MSP Stripe price IDs, and public signup enablement, and must enforce the MSP client-workspace cap fromCP_PROVIDER_MSP_PLAN_VERSIONusing the canonicalmsp_starter,msp_growth, andmsp_scaleplan versions. Provider-hosted portal bootstrap/accounts may have no hosted billing record, and that absence must remove Pulse-hosted Billing navigation, support actions, and role copy rather than exposing dead Billing controls. Shared-process multi-tenant mode (PULSE_MULTI_TENANT_ENABLEDplusFeatureMultiTenant) remains an Enterprise/internal-organization capability; it is not the canonical MSP route for separate customer businesses and must not leak MSP terminology, controls, prompts, or setup requirements into ordinary self-hosted Pulse. The canonical provider workspace limits are 5 client workspaces for Starter, 15 for Growth, and 40 for Scale; the canonical public monthly prices are $149, $249, and $399 respectively. Public copy may publish all three prices, but must keep Starter as gated checkout rather than a general self-serve provider-hosted purchase path, and describe Growth and Scale as request-assisted access rather than implying immediate self-serve provisioning. Larger providers belong to Enterprise/custom terms. Design-partner discounts do not create a public tier or change runtime limits: a qualified MSP lead may receive assisted Growth access at the Starter price for a limited 6-12 month field-feedback window, with up to 15 client workspaces and no self-serve checkout exposure. The MSP signup routes stay gated behind the samePublicCloudSignupEnabledflag as the individual cloud signup front door. Checkout metadata produced by the MSP front door must markaccount_kind=msp,signup_source=public_msp_signup, andcheckout_billing_mode=immediateso provisioning seeds an operator workspace as an immediately active paid account rather than an individual trial tenant. Individual public Cloud signup may keep using the hosted trial checkout helper; MSP public signup must not set Stripetrial_period_days. Hosted Cloud/MSP tenant runtime may be hibernated for cost control while the default launch motion stays provider-hosted. Hibernation may scale down or pause tenant containers, demo/proof tenants, tenant-only workers, and nonessential hosted observability, but it must preserve snapshots/backups, DNS/config/secrets references, and a documented wake path. It must not disable the commercial/account backbone: public site, license server, checkout/webhook fulfillment, license retrieval, refund/data-request fallbacks, Relay/mobile infrastructure for active users, or Pulse Account account-recovery surfaces. This policy records the allowed cost-control posture only; any live infrastructure shutdown still requires an explicit target inventory, rollback/wake runbook, and operator approval. - Add or change the hosted account portal API, Pulse Account access/auth/session handling, task-first browser shell, maintained portal frontend/bundle, or account-scoped workspace/access/billing handoff through
internal/cloudcp/account/audit.go,internal/cloudcp/account/handlers.go,internal/cloudcp/auth/handlers.go,internal/cloudcp/auth/session.go,internal/cloudcp/portal/, andinternal/cloudcp/routes.goThat same customer-entry boundary owns the canonical hosted Cloud handoff: public Cloud entry, secure checkout return, and returning-customer sign-in must converge on Pulse Account. Public Cloud signup stays canonical at/cloud/signup, returning commercial magic-link requests from hosted signup must targetportal, and hosted checkout provisioning must issue a portal-targeted magic link carrying tenant identity so the first authenticated landing is Pulse Account rather than a tenant-runtime-only redirect. Hosted tenant identity created from checkout, Pulse Account, or handoff must use the control-plane registry's stableUser.IDas the tenant org owner/member principal. Stripe customer email and SSO email are contact and delivery metadata only; they may be used for idempotent lookup or legacy fallback, but they must not become the primary hosted tenant user key when a stable account user ID exists. Hosted workspace provisioning with an owner email must create or resolve the account owner registry user before tenant org seeding. The Stripe provisioner must fail closed instead of synthesizingOwnerUserIDor memberUserIDfrom an email when no stable registry user exists. Public hosted signup must also repair older duplicate-signup rows at the provisioning boundary: if the existing org owner principal is blank or email-shaped, the hosted provisioner must generate a stableu_...owner, rewrite owner/member metadata, and grant the tenant admin role before returning the existing org for magic-link delivery. Hosted magic links are a delivery mechanism inside that same boundary: verification must re-read the tenant organization and session as the stored stable owner/member principal, so an old token cannot keep email as the runtime identity or survive removal from tenant membership. Stripe checkout webhook magic-link delivery must use that same organization resolver before sending the best-effort sign-in link; a webhook may activate billing for a server-linked org, but it must not send a sign-in link merely because Stripecustomer_emailmatches contact metadata with no stored principal. Tenant-targeted hosted magic-link handoff must fail closed when the control-plane registry cannot resolve a stable accountUser.ID; it must not sign a tenant handoff with a blank subject and let the tenant runtime promote contact email into identity. That same portal boundary also owns the signed-in shell shape: hosted arrivals default toWorkspaces, self-hosted-only arrivals default toBilling, and the shell destinations are limited toWorkspaces,Access,Billing, andSupportrather than a separateOvervieworSummarydestination. The top ofWorkspacesmay surface one quiet inline facts line plus one next-action row before the list, but it must not turn that summary into a second overview deck, metric grid, or competing shell. Pulse Account workspace handoff is the provider control-plane path into a client workspace, not a parallel monitoring surface. Workspace rows and selected-workspace panels may offer task handoffs such asOpen workspace,Install agents, and reporting, but those actions must stay POST handoffs through the account tenant route. Any task target must be sanitized and signed into the control-plane handoff token as a tenant-local path, then sanitized again by the tenant exchange before redirect. Agent install commands, alerts, and performance reports remain owned by the target tenant runtime; Pulse Account may deep-link to those tenant surfaces but must not mint workspace agent credentials or render cross-client monitoring state in the account portal. Pulse Account may surface tenant-local active alert rollups as counts and age labels from read-only setup facts so providers can prioritize the workspace list, but it must not become an alert console or expose alert bodies, remediation state, acknowledgements, or cross-client alert streams. Those setup facts must read report schedule counts from the client runtime's org-scopedreport_schedules.jsonstore and active-alert counts from the org-scopedalerts/active-alerts.jsonruntime file before falling back to a legacy tenant-root active-alert file, because tenant monitors own org-scoped runtime persistence. Pulse Account also owns the provider-facing setup progression for client workspaces: after workspace creation the portal should select the created workspace, reveal the setup job, and preserve workspace/target context in install or reporting handoffs so an MSP can continue the first-run flow without guessing where to go next. That setup progression is control-plane guidance, not a second monitoring plane. Workspace readiness must be derived from read-only tenant-local setup facts without creating tenant config, encryption keys, migrations, agent credentials, alert routes, or report schedules as a side effect. The provider shell may show an unfinished-setup queue, disabled-output diagnostics, workspace-scoped primary actions, and provider setup templates for MSP accounts, but those templates are guidance rather than configuration.Readyrequires at least one reporting agent, one enabled alert route, and one enabled report schedule; a failed latest health check remainsReviewahead of setup counts, and critical alert rollups outrank generic health/setup review in provider attention ordering. Local MSP onboarding previews should be scenario-backed portal bootstrap data, not static screenshots, so they stay grounded in the real portal shape as the bundle changes. Hosted provider workspaces may store agent install tokens in the tenant runtime root token store rather than the org-specific config directory. Portal setup facts must count only root tokens whoseOrgIDorOrgIDsmatches the workspace tenant ID, combine them with org-local setup facts, and never let one client's root token make another client look configured. MSP account surfaces may call account workspacesclients, but that is a customer-facing portal vocabulary choice over the same tenant/workspace lifecycle. Mixed-account Pulse Account sessions must label account surfaces clearly enough that MSP client wording, Cloud workspace wording, access role explanations, and lifecycle controls do not bleed across accounts. Read-only account members must not see destructive client lifecycle controls. Repeated customer hostnames are acceptable only because agent ingest and monitors stay tenant-isolated; an MSP flow that lets two clients reportpve1must keep those reports in separate tenant workspaces rather than centralizing host identity in the provider account. Portal workspace payloads are live-workspace surfaces, not registry history dumps: tenants indeletingordeletedstate must stay hidden from the browser bootstrap,/api/portal/dashboard, and workspace-detail API so soft-deleted runtime cleanup preserves billing/account continuity without presenting retired hosted workspaces as actionable customer state. Pulse Account self-hosted upgrade and pricing-preview copy must describe Community as included core monitoring plus paid Relay/Pro extras. It must not frame self-hosted checkout as buying unlimited monitoring or monitored- system volume. Relay pricing presentation should be annual-first at the existing public $39/year price, with $4.99/month as the secondary option, so Relay reads as an optional support/convenience tier rather than a pressure upgrade. Relay copy must describe secure remote access to the Pulse web UI, Pulse Mobile pairing for handoff, push notifications, and 14-day history; it must not imply that the native mobile app is a full monitoring dashboard until that product surface exists. - Add or change Stripe provisioning plan resolution through
internal/cloudcp/stripe/provisioner.go,internal/cloudcp/stripe/webhook.go, andpkg/licensing/stripe_subscription.go. Checkout-session provisioning must be explicitly classified as hosted before tenant creation:cloud_*andmsp_*plan versions may provision hosted runtime, public Cloud signup source may provision only when the plan is otherwise unresolved for staging compatibility, and self-hosted v5/v6 checkout metadata must be ignored by the Cloud control-plane webhook even if Stripe delivers the event there. - Add or change activation/grant lifecycle, release build helper gating, or dev-mode capability widening through
pkg/licensing/dev_mode_features.go,pkg/licensing/service.go,pkg/licensing/testing_helpers.go,pkg/licensing/grant_refresh.go, andpkg/licensing/revocation_poll.go - Add or change license-server transport through
pkg/licensing/license_server_client.goThat transport boundary must use HTTPS for non-loopback commercial endpoints, may allow plaintext only on direct loopback development targets, and must route outbound license-server fetches through the restricted HTTP client so DNS rebinding or private-network drift cannot silently reopen the boundary. - Add or change encrypted activation persistence through
pkg/licensing/persistence.goandpkg/licensing/activation_store.goThat persistence boundary must encrypt new state only with the persistent random key file and the current HKDF-based derivation. Machine ID may remain only as a compatibility loader for previously saved state, and legacy derivations must never become the write path again. The compatibility loader must keep every key material a real v5.0.x install could have sealedlicense.encwith: the raw machine-id file content exactly as read (untrimmed, trailing newline included), the hostname fallback, and the fixed v5 dev fallback, each tried only through the legacy sha256 derivation on decrypt. Licenses activated on v5.0.0 predate.license-keyand are never re-saved, so dropping or trimming any of those materials silently downgrades paying upgraders to Community. - Add or change hosted trial or self-hosted purchase-return token semantics
through
pkg/licensing/trial_activation.goandpkg/licensing/purchase_return.go - Add or change hosted signup provisioning through
internal/hosted/provisioner.goThat provisioning boundary owns owner-email idempotency for public hosted signup. Repeated signup requests for the same owner email must resolve to the existing tenant record instead of creating parallel orgs for one account identity. The idempotency key remains the contact email only at the provisioning edge. New public hosted signup orgs must generate an opaque stable owner user ID, and registry-backed hosted paths must resolve a registry user ID, before writing hosted orgOwnerUserIDor memberUserID; owner/member email fields preserve delivery and display contact metadata only. - Add or change hosted billing-admin presentation through
frontend-modern/src/components/Settings/BillingAdminPanel.tsx,frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx, andfrontend-modern/src/components/Settings/useBillingAdminPanelState.tsHosted billing-admin refresh semantics, availability gating, and billing table state stay cloud-paid owned, while the refresh button chrome must compose the frontend-primitivesButtonfamily instead of carrying a hosted-billing-local compact action shell. Hosted billing admin organization row actions follow the same ownership split: cloud-paid owns Suspend, Activate, Reload, tenant billing state, and mutation semantics, while frontend-primitives owns the sharedButtonchrome used by those row actions. - Add or change shared commercial plan/usage presentation through
frontend-modern/src/components/Settings/CommercialBillingSections.tsxandfrontend-modern/src/utils/commercialBillingModel.ts - Add or change organization billing and usage presentation through
frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx,frontend-modern/src/components/Settings/OrganizationBillingLoadingState.tsx, andfrontend-modern/src/components/Settings/useOrganizationBillingPanelState.ts - Add or change self-hosted Pro plan, recovery, and entitlement actions through
frontend-modern/src/components/Settings/ProLicensePanel.tsx,frontend-modern/src/components/Settings/ProLicensePlanSection.tsx,frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx, andfrontend-modern/src/components/Settings/useProLicensePanelState.tsOrdinary self-hosted trial acquisition is retired: these plan surfaces may show active historicalsubscription_state=trialentitlement state, but they must not turntrial_eligible,trial_eligibility_reason, or an expired trial marker into a default Pro CTA or banner. Plan action links with commercial upgrade destinations must compose frontend-primitives'UpgradeButtonLink; non-upgrade external commercial URLs must composeButtonLink.ProLicensePlanSection.tsxmust not recreate local anchor button shells for purchase returns, private-runtime actions, or plan-comparison CTAs. Self-hosted plan refresh, entitlement retry, activation, and clear-license actions are cloud-paid behaviors, but their button chrome must compose the frontend-primitivesButtonfamily instead of carrying Pro-license-local primary, secondary, or warning action class strings. The self-hosted Pro plan and activation-success surfaces must keep Patrol actions tied to current operator work: set Patrol control when no work is active, open Patrol for current work, or review a pending governed decision. Self-hosted Pro plan status and readiness payloads are support diagnostics: the default Plan surface must keep the current plan and Patrol mode action primary, while capability details render behind a disclosure for troubleshooting unavailable controls or missing capabilities. That disclosure must not duplicate Patrol-page work progress, decisions, verified outcomes, or external-agent setup readiness. Completed verified Patrol outcomes are history/telemetry and must not appear as a defaultPatrol historyCTA or aPatrol workverified status row in the Plan surface. Frontend presentation models must call this a Patrol control action/status boundary;operations-loopwording is only acceptable at backend API or telemetry compatibility edges. The Plan surface may fetch Patrol operator status from the compatibility/api/agent/operations-loop/statusendpoint only after the current self-hosted plan resolves to a Pro-family tier throughgetSelfHostedPlanDefinitionForBillingTier. Community and Relay plans must render their included capabilities without loading paid Patrol operator-status plumbing. - Add or change paid relay settings and pairing presentation through
frontend-modern/src/components/Settings/RelaySettingsPanel.tsx,frontend-modern/src/components/Settings/RelayPairingSection.tsx, andfrontend-modern/src/components/Settings/useRelaySettingsPanelState.ts. The retired Dashboard shell must not be restored to carry a Relay onboarding card or equivalent blanket upsell — relay discovery stays inside its owning settings surface. Public demo and other read-only presentation policy states must suppress relay setup and upsell onboarding instead of inviting pairing or commercial action from a governed non-manageable surface. Relay settings must also translate internal registration-token failures into an operator-actionable activation-required state. Customer-facing Relay settings must not render rawregister:or license-token-provider diagnostics from the relay client as the primary status message. Relay settings and public commercial docs must also keep Remote Access positioned as a Relay-and-higher capability, not a Pro-only feature, so the Relay tier remains a tangible standalone paid product. - Add contract tests where runtime and pricing need to stay aligned
- Add or change hosted browser org-context bootstrap through
frontend-modern/src/App.tsx,frontend-modern/src/AppLayout.tsx,frontend-modern/src/useAppRuntimeState.ts, andfrontend-modern/src/utils/apiClient.tsThat same hosted bootstrap boundary also owns the runtime-capability JSON shape that the app shell consumes before it decides whether organization chrome and multi-tenant routes exist.pkg/licensing/entitlement_payload.gomust preserve emptycapabilitiesandlimitsas JSON arrays, andfrontend-modern/src/useAppRuntimeState.tsplus the shared license API adapter must treat those collections as canonical arrays rather than lettingnullcollapse hosted browser bootstrap into a free-tier fallback that hides organization settings or discards a valid org context. Organization membership changes that arrive through the self-hosted invitation flow must therefore refresh org bootstrap through the sharedorganizations_changedapp-shell path instead of forking a second hosted org bootstrap or pricing-aware shell reload. SSO display labels consumed during that same security-status bootstrap must reuse the existing/api/security/statusresponse and must not add another hosted organization or commercial posture probe before the protected state bootstrap. App-shell navigation rendered byfrontend-modern/src/AppLayout.tsxmust also keep decorative icon titles out of tab accessible names, so hosted and self-hosted chrome announce the canonical tab label plus meaningful badge counts rather than duplicating branded SVG titles. The Patrol-owned utility destination remainsPatrolover the same/patrolroute; it must not alter hosted org bootstrap, entitlement loading, or commercial posture loading. A Patrol open-work badge in that shell may consume only the existing Patrol findings and live-approval read model after authentication; it must not read hosted billing state, trigger commercial-posture loading, affect organization visibility, or become an upgrade/acquisition cue. The same primary platform navigation must remember the last in-tab route state per platform, including query and hash, so route-owned filters such as Proxmox workload status survive switching to another platform tab and back. That memory is chrome route state only: it must validate the route still belongs to the selected platform before reuse and must not become hosted org bootstrap, entitlement, billing, acquisition, or cross-platform query state. The same AppLayout shell may contextualize the closed Pulse Assistant launcher around the current monitoring, Patrol, Alerts, or Settings route, but that launcher must remain a local product affordance. It must not read hosted billing state, alter org bootstrap, or become a commercial activation prompt while attaching current-view Assistant context. Retired operator top-level route compatibility infrontend-modern/src/App.tsxmust stay inside authenticated app-owned surfaces such as Infrastructure, Workloads, Storage, and Recovery. Those compatibility routes must not revive hosted signup, public Cloud trial, pricing, or commercial acquisition surfaces inside the local product runtime. - Keep the hosted account portal shell task-first and compact. Section
headers, billing action rows, and the maintained portal bundle under
internal/cloudcp/portal/may surface the facts an operator needs, but the shell should not spend vertical space on duplicate context-chip strips when the page header and section body already communicate that scope.Workspacesmay own one inline facts line and one inline next-action row at the top of the section, but that summary treatment must stay inside the same task surface instead of reviving a separate overview panel, summary strip, or metric deck ahead of the real workspace list. - Keep monitored-system volume out of the commercial product model.
Recognized self-hosted and hosted v6 tiers must treat legacy
monitored-system limit, continuity, or
max_monitored_systemsmetadata as scrub-only compatibility input rather than as support/audit policy or a customer-facing plan model. Community, Relay, Pro, Pro+, lifetime, grandfathered recurring, Cloud, and MSP plan labels render through their normal plan surfaces with no Usage subtab, finite policy banner, monitored-system limit row, grandfathered-floor summary, or pause-new-admissions copy from stale volume metadata. Retiredmax_monitored_systemspricing handoffs must land on the neutral self-hosted plan surface and be normalized to plan selection rather than a monitored-system usage ledger. That same self-hosted commercial boundary also owns the operator-value narrative across the in-app billing shell, Pulse Account handoff, public pricing contract, and owned upgrade reasons. Customer-facing self-hosted copy must keep the ladder explicit asCommunity = monitor,Relay = reach Pulse from anywhere plus pair Pulse Mobile for handoff, andPro = hands-on Patrol modes, issue investigation, verified fixes, and 90-day history. Team/admin extras such as RBAC, SSO, audit logging, reporting, and agent profiles may remain present, but they are secondary included value and must not displace that operator outcome framing on owned commercial surfaces. - Keep public-demo route bootstrap owned on the adjacent
commercial/runtime boundary.
frontend-modern/src/useAppRuntimeState.tsmay prewarm shared infrastructure summary caches for route-owned surfaces, but public-demo arrival must not front-run a broader infrastructure-summary fetch than the route actually renders. Commercial posture on the stable public demo therefore stays governed by the route-owned presentation policy and summary scope rather than by app-shell-wide bootstrap heuristics. That same app-shell boundary must also keep the public login entrypoints (/and/login) quiet before a session exists: when auth is configured but no local auth hint has been established yet,frontend-modern/src/useAppRuntimeState.tsmust stop at the shared login-needed state and skip/api/state, while protected-route arrivals and post-login reloads still keep the canonical state probe for runtime detection. Protected Patrol entrypoints follow that same route-owned rule: the canonical Patrol surface may live at/patrol, and any legacy/aibrowser entrypoint must remain only a thin authenticated redirect rather than a second public handoff or a route that invites separate commercial posture logic. - Keep self-hosted commercial plan copy aligned with the v6 operator-value
model instead of mirroring raw entitlement keys.
frontend-modern/src/ utils/selfHostedPlans.tsmay still map onto internal capabilities such asai_patrol,ai_alerts,ai_autofix, andkubernetes_ai, but the customer-facing plan cards, top-summary highlights, and comparison rows must lead with the canonical plan story:Community = monitor,Relay = reach Pulse from anywhere plus pair Pulse Mobile for handoff, andPro = hands-on Patrol modes, issue investigation, verified fixes, and longer history. Team/admin capabilities such as RBAC, audit logging, reporting, and agent profiles may appear as secondary included extras. Their customer-facing plan summary label isadmin controls, notadmin tools, because Pro is sold as Patrol handling infrastructure within a chosen mode rather than as a bag of operator utilities. Platform-specific compatibility keys such askubernetes_aimust not be elevated into a marquee marketed Pro line item. Pro comparison and plan-selection copy must frame Patrol around what it may handle automatically, issue investigation, policy-bounded action, verified fixes, and history rather than as an abstract governed operator or a choice about how much control Patrol has. When free-plan copy mentions Patrol, it must describe watch-only/background health checks; investigation, approval-backed fixes, autonomous action, outcome verification, and extended history are Pro value. Legacy packaging nouns such asincident memory,scheduled remediations, andexecution audit trailmust likewise stay out of current v6 commercial copy unless Pulse ships a first-class product surface that makes those names truthful again. The raw entitlement keyai_autofixmay remain the internal capability identifier, but owned plan cards, comparison rows, and upgrade reasons must present it as Patrol fixing safe issues within the selected Patrol mode rather than asPatrol Auto-Fixor an abstract remediation-workflow label. Pulse Account preview/handoff pricing copy, public plan docs, and the in-app self-hosted billing shell must share that same free-first ladder so release-candidate and public purchase paths do not drift into a separate commercial story. That same self-hosted pricing boundary now also owns canonical feature classification metadata.pkg/licensing/self_hosted_feature_catalog.gois the source of truth for customer-facing self-hosted feature labels, comparison visibility, plan roles (primary_pillar,included_extra,compatibility_only, and hidden/included states), and generic upgrade reasons.frontend-modern/src/utils/selfHostedFeatureCatalog.generated.tsandpulse-pro:license-server/self_hosted_feature_catalog.generated.goare generated projections of that source for app and public-pricing consumers; those projections must not drift through hand-maintained parallel lists or page-local renaming.pkg/licensing/upgrade.gomust also treatcompatibility_onlyentries as non-marketed compatibility capabilities by returning only the generic pricing destination rather than a feature-specific paid upgrade URL. - Keep hosted trial-activation verifier source selection compile-time owned.
pkg/licensing/trial_activation.go,pkg/licensing/trial_activation_public_key_override_dev.go, andpkg/licensing/trial_activation_public_key_override_release.gotogether define whether runtime environment may override the hosted verifier. Dev builds may keep the local override path, but release builds must resolve the verifier from the embedded build-time source of truth instead of honoringPULSE_HOSTED_MODEor other runtime wiring. - Add or change the Cloud control-plane runtime, public signup lifecycle,
tenant health/reconciliation, control-plane metrics, email delivery,
hosted handoff, hosted entitlement refresh, or hosted tenant reaping through
internal/cloudcp/andinternal/hosted/. Those paths must stay governed by the Cloud paid subsystem even when a narrower support subsystem also owns a deployment, security, or relay-specific file in the same package tree. Control-plane transactional email may use a no-reply sender for delivery, but Resend delivery must set an explicit support reply path fromPULSE_EMAIL_REPLY_TO, defaulting tosupport@pulserelay.pro, so magic links, checkout handoffs, and other hosted-account messages remain directly replyable by customers. Paid and hosted app shells must inherit the frontend-primitives-owned platform/runtime primary navigation contract and canonical path constants fromfrontend-modern/src/routing/resourceLinks.ts; cloud-paid must not carry a second copy of the platform list, shell order, or Machines-surface eligibility rules. Product navigation remains support-and-evidence gated:enabled,live, and inclusion in the rendered primary navigation derive from the governed support manifest plus canonical resource evidence instate.resources. Admitted-only, presentation-only, unsupported, or absent platform families must not render as disabled placeholders in the paid or hosted app shell. Infrastructure is not an equal primary tab in that list. Workloads, Storage, and Recovery are not standalone aggregate workspace tabs; their component surfaces remain reusable inside platform pages via embeddedtableOnlysurfaces. Platform primary-tab settings handoffs inAppLayout.tsxmust target the canonical/settings/infrastructureworkspace and must not retain retired settings aliases such as/settings/workloads/dockeror nested/settings/infrastructure/platforms/*paths. Each platform page must remain chrome-only: routing plus sub-tab navigation that embeds the canonicalWorkloadsSurface,StorageSurface,RecoverySurface, orUnifiedResourceTableinembedded tableOnlymode with a forced platform/source filter. The shell must not introduce dashboard cards, bespoke per-family tables, synthetic placeholder data, or reintroduce Infrastructure as a primary navigation entry without a governed contract decision recorded by the frontend-primitives owner. The presence of the user-facingMachinestab, backed by the internalstandaloneroute/id, is not a new commercial usage unit: hosted and paid surfaces continue to meter the governed monitored-system grouping result rather than counting primary navigation destinations. The user-facingPatrolutility tab is likewise not a commercial plan or usage unit; it remains the Patrol-owned daily work entrypoint over current findings, approvals, and verification state. Landing behavior for paid and hosted shells must also defer to the frontend-primitives-owned provider-first landing contract instead of defining a cloud-paid-specific order. - Introduce the self-hosted
businesstier only through this shape:TierBusinessinpkg/licensing/features.gocarries exactly the Pro feature set (no feature-level differentiation), 365-day history retention inTierHistoryDays, membership in the self-hosted core-monitoring-uncapped tier and plan-version sets (business,business_annual), theBusinessdisplay name, and a slot between Pro and MSP in the min-tier ordering. Business differentiates commercially by themax_userslicense limit (unlimited for Business; newly issued Pro licenses may carry a finitemax_userswhile previously issued licenses keep their unlimited posture), retention, and support, never by gating features away from Pro. The tier stays dormant until the license server issues business plan versions through a governed rollout; no checkout, pricing-model payload, public pricing page, or in-product plan surface may reference Business before that rollout, and monitored-system volume stays out of the Business plan model per the self-hosted commercial boundary above.
Forbidden Paths
- New ad hoc plan names in runtime or UI
- Silent aliases between old and new limit keys in live runtime paths
- Pricing/UI claims that are not enforced by runtime entitlements
Completion Obligations
- Update this contract when cloud plan semantics change
- Update runtime and frontend tests together when plan/limit rules move
- Add or tighten drift tests when a pricing/runtime mismatch is fixed
- Keep self-hosted pricing and public docs on runtime-backed commercial truth: self-hosted Patrol remains provider/local-model based in Community, paid discovery stays out of ordinary monitoring flows, and retired hosted-model credits or trial acquisition must not be presented as a default self-hosted benefit. Relay and Pro remain the canonical self-hosted commercial story.
- Treat grandfathered
lifetimelicenses as uncapped commercial entitlements: they keep the Pro feature set, but they must not inherit monitored-system or guest caps from recurring Pro contracts anywhere in runtime, issuance, or migrated-license storage. - Treat active recurring v5 Pulse Pro customers as grandfathered commercial entitlements until cancellation: migrated and renewing v5/v1 recurring plans keep their existing recurring price while the subscription remains continuous. The current Community / Relay / Pro self-hosted packaging also stays no-cap for monitored systems, guests, and child-resource volume, with Pro+ remaining legacy continuity only.
- Keep Pro+ app presentation continuity-only: customer-facing tier, plan, and
plan-version labels for
pro_plusmust include legacy framing while still mapping the entitlement to the Pro runtime feature set. - Keep persisted billing baselines and live recurring continuity scrubbed:
pkg/licensing/billing_state_normalization.go,pkg/licensing/database_source.go,pkg/licensing/models.go, and downstream runtime entitlement evaluation must delete retired monitored-system volume keys before runtime status, grants, leases, or frontend billing payloads are built. - Keep legacy migration fallback continuity compatibility-only on v6:
legacy_migration_fallbackmay be parsed so old activation files and billing records load, butplan_limit,grandfathered_floor, and related volume telemetry must not become runtime status, entitlement enforcement, customer-facing support context, or self-hosted Plans presentation. - Keep organization billing loading placeholders on the shared
SettingsLoadingSkeletonprimitive. Cloud-paid owns billing and usage semantics; frontend-primitives owns the loading skeleton animation, fill tokens, and metric/progress/card placeholder shells. - Keep Stripe webhook idempotency state bounded in the control-plane
registry:
internal/cloudcp/registry/registry.gomay retainstripe_eventsrows long enough to suppress duplicate deliveries and reclaim stale in-flight work, but it must prune expired processed or abandoned rows so webhook dedupe does not grow without bound on disk. - Keep the maintained Pulse Account portal bundle source-synced with
internal/cloudcp/portal/frontend/: any slice that changes the portal frontend hash or emitted manifest must rebuildinternal/cloudcp/portal/dist/build_manifest.jsonand keepinternal/cloudcp/portal/frontend_sync_test.gogreen in the same change. - Before GA, treat infrastructure monitoring volume as unmetered: monitored systems remain the canonical inventory grouping unit, but paid value must come from optional extras, hosted convenience, business workflow, support, or similar non-core surfaces rather than using monitored-system volume itself as a paid gate. Child-resource volume, including guest capacity, must follow the same self-hosted rule instead of becoming a replacement paid gate for core monitoring. Monitored-system impact-preview copy must follow the same rule: ordinary connection previews may describe count impact and grouping changes, but they must not use capacity-style titles or slash-style quota summaries that imply self-hosted monitoring volume is the product being sold.
- Keep v6 billing state uncapped even when persisted state still carries
legacy commercial volume-limit keys:
pkg/licensing/models.go,pkg/licensing/service.go,pkg/licensing/entitlement_payload.go,pkg/licensing/billing_state_normalization.go,pkg/licensing/database_source.go,pulse-pro:license-server/v6_store.go, andpulse-pro:license-server/v6_schema.gomust scrub stalemax_monitored_systemsvalues for Community/free, Relay, Pro, Pro+, Pro Annual, lifetime, eligible grandfathered recurring, Cloud, and MSP plan labels before runtime-capability, entitlement, grant, lease, billing, or browser-facing payloads are built. The same boundary must merge sparse legacy, configured-plan, or manually supplied feature lists with canonical recognized-tier defaults before storage, API response, or grant signing so Lifetime, Pro, Pro+, and grandfathered recurring customers cannot be downgraded to the partial feature list carried by an old JWT or legacy plan row. - Keep self-hosted commercial funnel stage ownership out of the customer
product runtime. The customer frontend must not contain
upgradeMetrics,conversionEvents, or infrastructure onboarding metrics wrappers or call sites at all. In-appPlans & Billing, pricing, checkout, paywall, or onboarding surfaces must not emit browser product events, and the normal self-hosted API must not register local upgrade-metrics ingestion, health, config, stats, or funnel routes.pulse-pro:license-server/v6_checkout.goowns the Pulse Account handoff equivalents bound toportal_handoff_id. Pulse must not infer those portal stages from referrer state, and the commercial service must keep those self-hosted handoffs on release trackv6even while the public site remains onv5before GA. - Keep retired local commercial funnel reporting out of customer diagnostics,
settings, and product startup:
internal/api/diagnostics.go,frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx,frontend-modern/src/components/Settings/diagnosticsModel.ts,internal/api/router_routes_licensing.go,internal/api/system_settings.go,internal/config/config.go,internal/config/persistence.go,pkg/server/server.go,frontend-modern/src/stores/systemSettings.ts, andfrontend-modern/src/types/config.tsmust not expose maintainer funnel, pricing/checkout, upgrade-metrics, or infrastructure-onboarding analytics fields, controls, routes, stores, or startup DB artifacts to normal product users. - Keep ordinary self-hosted v6 commercial prompts opt-in. Cloud-paid runtime
may keep checkout, activation, recovery, and support-only trial plumbing
available for explicit handoffs and entitled installs, but default
self-hosted browser surfaces must honor
presentationPolicy.hideUpgradeand suppress Relay/Pro plan comparison, Pro trial CTAs, monitored-system limit pressure, paid-only settings navigation, and feature upsells unless hosted mode, direct intent, activation/recovery state, or active entitlement makes them relevant. The authenticated app shell must also skip background/api/license/commercial-posturebootstrap whilepresentationPolicy.hideUpgradeis true; explicit self-hosted plan, activation/recovery, and hosted or prompt-allowed flows may still refresh the shared posture store. - Keep hosted and trial billing construction separate from retired hosted-AI
quickstart inventory:
pkg/licensing/trial_start.goand hosted entitlement refresh paths may preserve historical billing fields for old state, but new trial or hosted workspaces must not mint quickstart credits or imply a managed-model allowance as part of the commercial contract. - Keep Cloud signup acquisition owned by the Cloud control plane and public
account surfaces, not by the ordinary self-hosted app shell. The default
self-hosted frontend must not register unauthenticated
/cloudor/cloud/signuproutes, and self-hosted pricing fallbacks must route Cloud interest to Pulse Account/public Cloud ownership rather than rendering an in-product trial or checkout page. - Keep the
unlimitedfeature key neutral in shared metadata. It is a hosted/MSP capacity-policy marker, not a self-hosted monitoring-volume product promise. Runtime and generated feature catalogs must label it as hosted capacity policy, keep it hidden from self-hosted plan cards, and avoid customer-facing "Unlimited Instances" copy that sounds like the old capped self-hosted packaging.
Current State
Primary nav moved to platform-first on 2026-05-16 through
frontend-modern/src/App.tsx, frontend-modern/src/AppLayout.tsx, and
frontend-modern/src/pages/RuntimeHome.tsx. The authenticated-runtime landing
redirect now resolves to /proxmox/overview via buildProxmoxPath() rather
than the retired /infrastructure shell; kiosk-mode blocked-prefix redirects
follow the same canonical home. The hosted bootstrap / activation / recovery
routes (/pricing, /preview/setup-complete, /login, settings panels
under /settings/...) are unchanged. Pricing-handoff and hosted-signup
upgrade paths must use platform routes as their canonical "back to product"
destination instead of the retired top-level Infrastructure route; aggregate
Workloads / Storage / Recovery pages are product workspaces, not hosted
acquisition destinations. The Docker / Podman runtime route remains /docker
for search familiarity, but the shell may label the
destination as Docker so it reads as the recognizable container runtime lens
rather than an owning infrastructure platform.
Localized runtime-home loading copy may route through the shared frontend i18n
catalog, but this does not move route ownership or commercial posture into
localization: authenticated /, hosted workspaces, public-demo suppression, and
entitlement-read prohibitions remain governed by this cloud-paid boundary.
The Proxmox platform tab in the authenticated app shell is ordinary product navigation. It must stay separate from hosted acquisition, monitored-system pressure, billing prompts, and Pulse Account handoffs: self-hosted operators may open the Proxmox page without seeing commercial upgrade copy, while hosted commercial policy continues to render through the existing cloud-paid banners and settings surfaces.
frontend-modern/src/utils/pricingHandoff.ts now routes displayable paid
self-hosted feature keys (relay, mobile_app, push_notifications,
ai_alerts, ai_autofix, long_term_metrics, rbac,
audit_logging, advanced_reporting, agent_profiles) to the
self-hosted billing plan page instead of the Pulse Account purchase-start
handoff. The purchase-start handoff requires a PublicURL and fails on local
instances; routing these keys to the in-product billing plan keeps upgrades
accessible from self-hosted environments.
Retired trial-acquisition intents such as trial_expired must not be owned
pricing handoff destinations; no ordinary self-hosted runtime path should emit
them as an upgrade reason or plan-page CTA.
Legacy browser arrivals carrying that stale feature may land on the neutral
self-hosted plan surface, but must not preserve trial_expired into a Pulse
Account purchase-start handoff or trial-shaped checkout feature. The shared
upgrade-action fallback must therefore return no action for retired trial keys,
while the legacy /pricing?feature=trial_expired browser route may strip the
stale feature and route to /settings/system/billing/plan.
The monitored-system app-shell warning CTA now follows that same commercial
boundary by rendering only in hosted mode. Ordinary self-hosted installs must
not see finite monitored-system pressure in the global app shell; when hosted
capacity policy is active, the banner reviews finite-policy usage on the usage
ledger rather than sending operators to the plan-selection surface with
capacity-shaped copy.
Stripe checkout metadata now treats plan_id and price_id as Stripe price
identifiers for plan-version derivation, including grandfathered v5 recurring
prices. The Cloud control-plane webhook uses that derivation as an admission
boundary: hosted Cloud/MSP checkout events may provision tenants, while
self-hosted landing purchases are acknowledged and ignored rather than
creating hosted containers or Stripe account mappings.
The Pulse-operated MSP account path has a gated public front door alongside the
individual cloud signup page. internal/cloudcp/public_msp_signup_handlers.go
serves /cloud/msp/signup, /cloud/msp/signup/complete, and
/api/public/msp/signup, all gated behind the same PublicCloudSignupEnabled
flag as /cloud/signup, so the MSP front door stays dark until an operator
explicitly enables public signup. This path is not the provider-hosted MSP
artifact; it is the Pulse-operated checkout/account entry point that proves the
provider-account/client-workspace model. MSP Starter is the only tier that may
be served through gated checkout, and it is offered only when
CP_MSP_STARTER_PRICE_ID is configured. Growth, Scale, Enterprise, manual
pilots, and discounts belong in request-based or operator-controlled
Stripe/customer-support workflows rather than the public front door. The signup
page renders an explicit "Starter self-serve signup is not open yet" notice when
the Starter price is not configured rather than offering an unbacked checkout.
Checkout sessions started from this front door carry account_kind=msp,
signup_source=public_msp_signup, and checkout_billing_mode=immediate
metadata and do not set Stripe trial_period_days; any opened checkout is
paid-first. The provisioner reads that billing-mode metadata so the initial
Stripe account mapping is stored as active instead of trial before subscription
webhooks catch up.
Cloud paid readiness is materially behind architecture work. The main concern is
contract coherence between pricing, entitlements, and runtime enforcement.
Pulse Account portal workspace copy is part of that same readiness contract:
hosted view-only users may be invited to review workspace health and open ready
workspaces, but workspace section lead copy must route lifecycle handling to an
owner or admin instead of presenting Lifecycle as a live task for the current
signed-in role.
That public-demo commercial boundary also owns monitored-system preview
unavailability wording. Browser presentation may keep the unavailable reason
nullable until the formatting edge, but it must normalize the message through
the shared monitored-system presentation helper instead of branching on
demo/billing state inside settings panels or inventing a second mock-only
license explanation path.
That same cloud-paid/browser boundary now also governs public demo posture.
DEMO_MODE may run against a real internal entitlement, but public demo
surfaces must not reveal self-hosted license metadata, hosted billing state,
monitored-system ledgers, upgrade nudges, or activation controls just because
the underlying runtime is commercially enabled.
That same cloud-paid boundary now also owns release-demo fixture secrecy.
Release builds may enforce the internal demo_fixtures entitlement before
enabling mock fixture runtime state, but browser-visible licensing and
commercial payloads must still filter that capability back out and must not
let cloud-paid surfaces treat dev/test fixture toggles as plan, billing, or
upgrade truth.
PULSE_DEV and PULSE_MOCK_MODE may remain backend-only development or
fixture gate bypasses for local/runtime operations, but
pkg/licensing.Service.Status() must not use either flag to synthesize
customer-facing capabilities, limits, plan state, upgrade reasons, or paid
settings navigation in /api/license/status,
/api/license/runtime-capabilities, or /api/license/entitlements.
That same resolved presentation policy also governs hosted organization chrome
inside the demo/browser shell: app bootstrap may retain an internal default
org context for hosted API routing, but frontend-modern/src/useAppRuntimeState.ts,
frontend-modern/src/App.tsx, and frontend-modern/src/AppLayout.tsx must
not render visible org switchers, Default Organization labels, or
organization-scoped navigation just because a seeded hosted org still exists
behind the session.
internal/api/router_routes_auth_security.go,
internal/api/security_status_capabilities.go,
frontend-modern/src/useAppRuntimeState.ts, and
frontend-modern/src/stores/sessionPresentationPolicy.ts must treat
/api/security/status as the canonical browser bootstrap contract. The
backend capability fact sessionCapabilities.demoMode still seeds that
contract, but shared billing and upgrade surfaces now hide or suppress
themselves from the resolved presentationPolicy payload instead of teaching
mock mode, response-header inference, or frontend-only feature flags to
bypass the real licensing model. That includes settings billing tabs,
public-demo banner and monitored-system/trial nudges, app-wide relay
paywalls, Patrol upgrade CTAs, and history-lock upsells. Demo readiness
therefore means presentation isolation, not a license exemption.
That same public-demo boundary now also owns route-level commercial
classification. internal/api/demo_middleware.go and
internal/api/demo_mode_commercial.go must decide centrally which commercial
endpoints are fully hidden (404) and which remain available only as a
non-commercial public contract. /api/license/runtime-capabilities is the
canonical public exception for feature truth and history retention.
/api/license/commercial-posture is the canonical non-billing commercial
contract for upgrade posture in real customer workspaces, while
/api/license/entitlements remains billing-only. In public demo mode both
commercial routes, plus /auth/license-purchase-start, must stay hidden and
public browsers must not see licensed identity, plan labels, upgrade reasons,
trial urgency, observed usage counts, or checkout handoff state. The
commercial posture store and billing-entitlements store must also fail closed
locally until the shared presentation policy resolves, then stay fail-closed
in demo mode so hidden routes are not probed from the browser shell.
Paid self-hosted Pro licenses also have a runtime-distribution boundary:
billing entitlement payloads may confirm the paid feature grant, but the
runtime-capabilities payload must mark private-runtime-only capabilities as
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
canonical Settings support navigation, and public demo mode must keep those
support-only entries hidden instead of leaving the retired operations surface
discoverable after commercial surfaces are hidden.
Deep-linkable commercial panels must consume the same resolved presentation
policy directly, not rely only on settings navigation to keep public-demo
browsers away from commercial routes. ProLicensePanel may instantiate its
license/trial/recovery state only after the policy resolves and only when
commercial surfaces are visible. MonitoredSystemLedgerPanel must also defer
ledger reads until that policy resolves and suppress them while commercial
surfaces are hidden. Public demo mode therefore renders a redacted
presentation-policy state instead of creating a fake entitlement, probing
hidden commercial endpoints, or showing monitored-system usage pressure.
That same commercial/public browser boundary also owns pricing-handoff
framing. frontend-modern/src/pages/PricingHandoff.tsx may keep the
operator-visible handoff on /pricing, but it must render the shared
PageHeader shell while frontend-modern/src/utils/pricingHandoff.ts
continues to own destination resolution, self-hosted Pulse Account handoff,
and public-pricing fallback truth. The route must not fork a raw top-level
heading, duplicate destination logic in the page shell, or let commercial
handoff framing drift away from the same shared browser chrome used by the
rest of the product.
The visible redirect title and manual-link copy on that page may use the
shared frontend i18n catalog for first-wave locale coverage, but the
destination selection, self-hosted GET /auth/license-purchase-start handoff,
public pricing URL fallback, feature query keys, and purchase-return states
remain cloud-paid-owned and machine-stable.
The governed browser proof for that posture lives in
tests/integration/tests/53-demo-mode-commercial-boundary.spec.ts and is
expected to stay runnable through
tests/integration/scripts/run-tests.sh demo-contract.
That same public-demo boundary now treats the old v6-demo.pulserelay.pro
preview route and its pulse-v6-preview service/bootstrap tooling as retired
after v6 GA. The stable public demo is the canonical customer-facing v6 demo
surface. Any future prerelease or canary demo needs a newly governed target,
hostname, service identity, and audit path; it must not silently revive the
retired v6-preview route or split the public demo posture back into parallel
stable and v6 surfaces.
That same licensing/browser boundary now also owns authenticated commercial
posture bootstrap and the prohibition on non-billing entitlement reads.
frontend-modern/src/useAppRuntimeState.ts is the canonical authenticated
owner for the first /api/license/commercial-posture read when upgrade prompts
are allowed, while
frontend-modern/src/AppLayout.tsx,
frontend-modern/src/components/Settings/Settings.tsx,
frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts, and
other non-billing feature hooks may consume the resolved posture store but
must not each trigger their own mount-time posture fetch. Ordinary self-hosted
sessions where presentationPolicy.hideUpgrade is true must not perform that
background posture read from the app shell at all. Billing-owned
surfaces such as frontend-modern/src/components/Settings/useProLicensePanelState.ts
may still force refresh through the same shared store when plan, activation, or
recovery actions mutate commercial truth.
Non-billing browser journeys must also stay off
/api/license/entitlements entirely. Infrastructure, Workloads, alerts,
first-session gated routes, and relay settings must take feature truth from
/api/license/runtime-capabilities or already loaded commercial posture, and
the governed browser proof in
tests/integration/tests/11-first-session.spec.ts,
tests/integration/tests/journeys/01-smoke-bootstrap-login-infrastructure.spec.ts,
and tests/integration/tests/journeys/03-relay-pairing.spec.ts must continue
to assert zero browser-shell entitlement requests outside owned billing flows.
That same authenticated-shell commercial boundary may surface prerelease
guidance only as a thin release-metadata consumer. frontend-modern/src/AppLayout.tsx
may mount a shared release-candidate callout when the resolved version channel
is rc, but that shell copy must stay non-billing: it may link to release
notes and feedback, yet it must not probe billing endpoints, expose licensed
identity, or infer upgrade pressure from plan state just to explain the
current prerelease.
That same entitlement boundary also owns internal demo-fixture grants.
FeatureDemoFixtures may be issued only for governed internal demo runtimes,
must never join public tier defaults or pricing contracts, and must be
filtered back out of browser-facing capability and entitlement payloads even
when the runtime uses it to authorize release-build mock fixtures.
That same browser/store boundary also owns typed non-billing commercial
selectors. frontend-modern/src/stores/licenseCommercial.ts may interpret the
commercial-posture payload once for browser consumers, but relay, Patrol,
trial banners, monitored-system warnings, and settings paywall state must not
branch directly on raw subscription_state, trial_eligible,
trial_days_remaining, or overflow_days_remaining fields in each leaf
surface.
Legacy Cloud plan aliases are now expected to canonicalize to the cloud_*
contract not only when Stripe metadata is parsed, but also when persisted plan
versions are consumed at hosted entitlement and workspace-limit enforcement
boundaries.
That same hosted browser bootstrap boundary now owns browser-only lifecycle
attachment too. frontend-modern/src/useAppRuntimeState.ts must register its
auth bootstrap, theme sync, and post-reconnect hosted refresh work through
Solid onMount(...) inside the runtime owner instead of letting App.tsx,
AppLayout.tsx, or module-evaluation side effects reach directly into browser
APIs before the hosted shell has mounted.
That same hosted browser shell boundary also owns same-path query-state
transitions that refresh hosted/org context without changing the active page.
frontend-modern/src/App.tsx must preserve the mounted .app-scroll-shell
position across those remount-like transitions so inline row focus, hosted
drawer state, and org-context route updates do not present as a full-page
refresh.
Persisted billing state is now also part of that canonical boundary: when a
recognized Cloud/MSP plan version is loaded or saved, the stored plan_version
must canonicalize and retired monitored-system volume limit keys must be
scrubbed rather than reconciled back into runtime state.
That same persisted billing boundary now also applies to hosted entitlement
lease secrets: internal/config/billing_state.go may keep EntitlementJWT
and EntitlementRefreshToken in runtime billing state, but billing.json may
not persist either value raw. Canonical persistence must keep the hosted lease
JWT and refresh token encrypted at rest, rewrite legacy plaintext billing files
on load, and drop those secrets instead of preserving plaintext-at-rest billing
state if encryption cannot be established. Empty/no-secret billing state may
not auto-create new crypto state just to add integrity metadata; no-key
graceful degradation remains canonical until a real billing secret or real
encryption key exists.
That same hosted billing boundary also owns base-path resolution:
internal/config/billing_state.go and
internal/api/payments_webhook_handlers.go must derive their base data
directory from the shared runtime data-dir helper in
internal/config/config.go instead of each carrying a private /etc/pulse
fallback. Hosted billing leases, webhook dedupe state, and customer indexes
must therefore follow the same configured runtime data-dir authority as the
rest of the product.
The same secret-at-rest rule also applies to activation state persistence:
pkg/licensing/activation_store.go may keep InstallationToken and
GrantJWT in runtime activation state, but activation.enc may accept
plaintext only as migration input. Once a legacy plaintext activation file can
be read, canonical persistence must rewrite encrypted storage immediately on
load instead of treating plaintext as a steady-state runtime path.
That same rule also applies to the canonical local license state in
pkg/licensing/persistence.go: license.enc may carry the commercial license
key and grace-period metadata in runtime state, but a legacy plaintext
license.enc may only serve as migration input. Once it can be read,
canonical persistence must rewrite encrypted storage immediately on load
instead of treating plaintext licensing state as a valid steady-state path.
That same local-persistence boundary also owns the filesystem path contract for
commercial secrets at rest. pkg/licensing/persistence.go and
pkg/licensing/activation_store.go must normalize the owned config directory
once and resolve only the fixed .license-key, license.enc, and
activation.enc leaves through the shared storage-path helper before any
filesystem read, write, rename, stat, or delete. Future licensing persistence
changes must not bypass that resolver with raw filepath.Join(configDir, ...)
joins or introduce caller-controlled persistence filenames.
That same local-persistence boundary also owns writable-but-not-owned runtime
storage semantics for commercial state. pkg/licensing/persistence.go may
harden directories it owns to 0700, but it must not assume it can chmod the
root of a writable Kubernetes or container-mounted runtime data directory. When
the mount root is writable but not owned by the Pulse process, canonical
persistence must keep file-level secrets hardened at 0600, validate that the
resolved storage root is still the expected real directory rather than a symlink
or other filesystem object, and continue operating instead of crashing just
because the mount root itself cannot be chmod-ed.
Hosted entitlement-source loading follows the same rule: DatabaseSource must
normalize persisted Cloud/MSP plan aliases and legacy limit keys before runtime
evaluation, but it must not fabricate a canonical plan_version from bare
subscription lifecycle state when the stored plan label is absent.
Stripe control-plane fallback paths are also part of the boundary: when
subscription or workspace provisioning logic reuses an already stored
plan_version, it must canonicalize that value before persisting tenant,
Stripe-account, or billing-state updates.
Public cloud self-serve signup follows the same rule: internal/cloudcp/config.go
and internal/cloudcp/public_cloud_signup_handlers.go must accept only
canonical cloud_* Stripe prices for public hosted signup tiers and fail
closed when misconfigured so self-hosted or prelaunch-only prices cannot leak
into live public checkout flows. Until the governed v6 cutover explicitly
opens that path, internal/cloudcp/routes.go and the hosted portal bootstrap
must also keep public cloud signup links and route registration disabled even
if canonical Cloud price IDs are already present for release-day readiness.
That prelaunch gate must not disable /api/public/magic-link/request, because
existing hosted commercial accounts still depend on the public portal sign-in
flow before v6 public Cloud checkout is opened.
Signed hosted entitlement leases are part of the same boundary: lease signing
and verification must canonicalize recognized Cloud plan aliases and delete
retired monitored-system volume keys instead of trusting stale embedded values.
They also must not fabricate plan_version from bare subscription_state
when the signed lease claim label is absent.
The control-plane registry is also canonical: tenant and Stripe-account
plan_version rows must canonicalize recognized Cloud aliases on read and
write so stored legacy values cannot re-enter provisioning, entitlement, or
limit-enforcement fallbacks.
JWT-backed entitlement claims are also canonical: when runtime evaluation uses
claim plan_version and limits, recognized Cloud plan aliases must
canonicalize and retired monitored-system volume keys must be removed instead
of trusting stale embedded claim values. When a Cloud/MSP claim arrives without
a recognized plan label, runtime must preserve the missing/unknown
plan_version metadata while still treating monitored-system volume as
unmetered.
Activation-grant translation is part of the same boundary: when relay/license
server grants enter the local claims model, Cloud plan keys and lifecycle state
must still resolve through the canonical entitlement claim accessors rather
than becoming a parallel truth path.
The legacy-license exchange transport is part of that same activation boundary:
pkg/licensing/activation_types.go and pkg/licensing/license_server_client.go
must treat legacy_license_token as the canonical v6 request field for
POST /v1/licenses/exchange, while accepting legacy_license_key only as a
backward-compatible decode alias for older local stubs and historical test
fixtures. Future exchange-path changes must not reintroduce a split contract
where the shared Pulse runtime and the real pulse-pro/license-server disagree
on the activation payload shape.
That same license-server transport boundary now treats Patrol quickstart
bootstrap as retired, not as a mixed-version runtime extension point.
pulse-pro/license-server must not register /v1/quickstart/* routes, parse
model-provider quickstart env, or create new quickstart ledger tables. The
Pulse runtime must not call POST /v1/quickstart/bootstrap, must not persist
quickstart-backed AI config, and must not mint hosted-model tokens from hosted
or self-hosted billing state. Historical quickstart credit fields may remain
parseable in billing state only so old HMAC-protected files can still load.
The self-hosted commercial counted-unit contract is retired. Monitored systems
remain a canonical inventory grouping and support-debugging concept, but
max_monitored_systems, legacy max_agents, and legacy max_nodes are
decode-only compatibility inputs at the storage, grant, or purchase boundary.
Runtime enforcement, entitlement payload current usage, checkout/activation
flows, and upgrade messaging must not treat monitored-system volume as a cap.
Portal shell copy, pricing explainers, and upgrade helper text must focus on
paid feature value rather than monitored-system expansion.
That same retired counted-unit contract still owns prospective grouping
preview. Proxmox/PBS/PMG config adds, TrueNAS adds, VMware inventory previews,
and equivalent updates may ask the canonical monitored-system projection how a
connection groups into top-level systems before persistence, including
replacement-aware projections for source swaps on an existing grouped host,
but those previews are informational and cannot block a save for commercial
volume reasons.
The monitored-system ledger settings surface now also explains those grouping
decisions. Support UI may show grouped monitored systems, but it must render
the canonical backend explanation for why one or more top-level views group as
a single monitored system instead of inventing support copy or merge heuristics
in the frontend.
That billing support surface must also remain readable while mixed-version
clients and servers roll forward: missing explanation payloads may degrade to a
safe generic explanation, but the monitored-system ledger must never fail the
page or hide counted systems because the nested support details are absent.
That same monitored-system presentation boundary now also owns the customer-
facing ledger loading and retry copy through
frontend-modern/src/utils/monitoredSystemPresentation.ts, so commercial
usage states do not leak back into lifecycle inventory helpers.
That same cloud-paid boundary also owns the shared entitlement and upgrade
presentation helpers through frontend-modern/src/utils/licensePresentation.ts
and frontend-modern/src/utils/upgradePresentation.ts. Commercial tier labels,
feature minimum-tier messaging, migration/trial notices, billing-admin status
labels, and generic upgrade CTA labels must extend those helpers instead of
being forked into per-surface status-code branches that drift from backend
error truth. Button-styled upgrade CTA chrome is owned by
frontend-primitives' UpgradeButtonLink; commercial plan surfaces may choose
the destination, label, and emphasis but must not recreate local Tailwind
button anchors. The old self-hosted trial-start acquisition path is not a
current v6 commercial surface; denial and retry copy for ordinary self-hosted
runtimes must be limited to owned purchase, recovery, portal, Cloud signup, or
support handoff paths.
That same helper boundary also owns generic settings-paywall CTA labels. Under
the v6 free-first self-hosted policy, non-billing feature gates must stay
factual and route deliberate commercial intent to Plans with neutral "View
plans" copy; they must not start a Pro trial directly or embed local
Upgrade to Pro / Start free trial strings. Direct trial-start actions stay
retired for ordinary self-hosted v6; explicit commercial intent routes through
plan comparison, purchase activation, recovery, Cloud, or support handoff
surfaces where the operator has already chosen that path.
The same shared self-hosted commercial presentation boundary also owns the
recovery-surface copy for SelfHostedCommercialRecoverySection.tsx,
including the existing-key field label/help text and legacy-key exchange
notice, while selfHostedBillingPresentation.ts owns the first-class plan and
license-state copy that belongs on the primary billing surface. That split
prevents manual key redemption from drifting back into the main purchase path.
The recovery key input is also a frontend-primitives consumer:
SelfHostedCommercialRecoverySection.tsx must compose FormTextarea for the
manual license-key field so label/id/help wiring and textarea chrome remain
shared while cloud-paid continues to own the commercial copy and recovery
actions.
That same recovery boundary also owns the linked legal surface:
SelfHostedCommercialRecoverySection.tsx must route its Terms-of-Service link
through the shipped TERMS.md docs asset instead of sending operators to
GitHub main, so the recovery trust surface stays version-matched and
available on restricted installs. The anchor shell, rel policy, and compact
link styling for that legal/docs link belong to frontend-primitives'
ExternalTextLink; cloud-paid owns the commercial recovery copy and
destination semantics.
Already-issued legacy hosted trial leases are also part of that same
compatibility contract. If such a lease is refreshed, it must carry the
canonical Pro capability set while scrubbing retired monitored-system volume
limits rather than relying on downstream runtime fallback to infer commercial
state from trial metadata.
The top-level authenticated shell is part of that same customer-facing
boundary: cloud-paid trial prompts may appear in owned commercial surfaces, but
the app shell must not force a global, persistent Pro trial nudge that
overrides the primary runtime chrome for every signed-in user.
The maintained Pulse Account portal frontend is part of that same boundary for
local development as well as runtime delivery. Styling or interaction work on
internal/cloudcp/portal/frontend/ must have a local preview path that uses
the same frontend build pipeline as the embedded production bundle, while
serving scenario-backed local portal APIs without requiring a live cloud deploy
for every iteration. Final verification still belongs on a real control-plane
instance, but local portal design work must not depend on redeploying
cloud.pulserelay.pro just to see each frontend change.
That same preview/runtime boundary also owns shared browser chrome such as the
portal favicon: the local preview must serve the same /favicon.svg asset as
the real control-plane route so icon changes can be reviewed locally before
deployment instead of appearing only after a live push. The portal page itself
must also reference that shared favicon through a versioned href so updated
icon revisions bypass browser cache on deploy instead of waiting for asset
expiry.
That same frontend delivery boundary must keep the account portal visual language sharp and restrained, avoiding gradients, heavy shadows, decorative SaaS styling, and shell chrome that dominates the active task. The canonical target is a calm, flat account-operations surface closer to GitHub Settings or Android Settings than to a dashboard: a compact identity bar (account name, role, kind), a horizontal tab bar for section navigation, and direct content panels for Workspaces, Access, Billing, and Support. Shell framing uses white backgrounds, 1px borders, minimal border-radius, and hierarchy driven by spacing and typography rather than cards, pills, or ornamental headers. Section block headers render inline actions only inside the data container toolbar row rather than floating orphaned above content; fact chips, summary strips, decorative kicker labels, and redundant section headings do not appear in the primary task surface. Action buttons (Create workspace, Invite people, Change roles, Remove access) are integrated into toolbar rows within their respective bordered data cards rather than existing as free-floating elements above the content.
That same portal delivery boundary also owns the checked-in embedded bundle in
internal/cloudcp/portal/dist/. Visual or interaction changes are not
complete when they exist only in the local preview runtime or in hand-edited
generated output. The canonical edit path remains the frontend source under
internal/cloudcp/portal/frontend/, and any ship-ready portal styling change
must regenerate the embedded bundle plus the source-hash manifest so the live
control-plane, embedded Go template, and local preview all run the same
frontend revision.
That same shell framing also owns user-facing prerelease labeling for
rc-channel builds. frontend-modern/src/AppLayout.tsx may still key off the
canonical rc channel metadata internally, but the visible badge must frame
those builds as a preview/prerelease experience rather than implying a
near-ready release candidate. That authenticated shell must not pair the
preview label with a second top-of-shell release-candidate warning banner or
release-feedback CTA in frontend-modern/src/AppLayout.tsx; prerelease cloud
posture stays a subtle shell label, not a public-RC callout inside the paid
runtime chrome.
The self-hosted trial-start frontend runtime is retired for v6 GA. Commercial
relay, onboarding, setup, Pro settings, and shared paywall surfaces may still
offer explicit plan, activation, recovery, support, or hosted handoff paths, but
they must not revive local startProTrial() branches, shared trial-start
helpers, or global trial acquisition banners in the normal self-hosted app.
The same commercial handoff rule also covers the legacy /pricing route in
frontend-modern/src/pages/PricingHandoff.tsx and
frontend-modern/src/utils/pricingHandoff.ts: compatibility handoff may keep
product-owned destinations such as monitored-system billing or Cloud plans
inside the app, but self-hosted commercial upgrade intents now hand off to
Pulse Account first instead of treating the public pricing page as the app's
canonical destination. Pulse Account now owns self-hosted plan comparison,
checkout-session creation, and the purchase return handoff back into Pulse via
/auth/license-purchase-activate; Pulse itself must not render a second
public pricing surface inside the runtime, and the authenticated portal shell
must not collapse back into a public-site-only waystation for new-purchase
depth. That handoff now starts through Pulse-owned GET /auth/license-purchase-start,
which mints a signed purchase_return_token, creates a commercial-owned
portal_handoff_id that resolves to the bound checkout intent plus Pulse
success/cancel targets, and sends only that opaque portal_handoff_id to
Pulse Account before the browser leaves for the portal. Pulse Account
must resolve that server-owned handoff through the shared commercial API
before it starts checkout, so the portal never trusts browser referrer state,
loose feature / return_url query parameters, or a raw browser-supplied
checkout_intent_id for self-hosted purchase completion. The portal proxy
must expose GET /v1/checkout/portal-handoff as the canonical browser
bootstrap, and the retired GET /v1/checkout/intent bootstrap must stay
removed once portal_handoff_id is the owned contract. The browser-facing
portal handoff response must reveal only portal-owned bootstrap metadata such
as portal_handoff_id, feature, handoff lifecycle state, resolve timestamp,
and expiry; it must not disclose the bound checkout_intent_id, and browser
checkout creation must post only portal_handoff_id so Pulse Account
resolves the private checkout intent server-side before contacting Stripe.
For self-hosted upgrades, that browser-facing feature metadata is now
canonically self_hosted_plan. Pulse Account may continue normalizing the
legacy max_monitored_systems alias only for already-created checkout records,
but in-product browser links and fallback helpers must route that alias to the
neutral self-hosted plan surface instead of the monitored-system
usage/counting-rules surface. The portal proxy contract, checked-in embedded
bundle, and rendered upgrade copy must treat self-hosted commerce as plan
selection and paid extras rather than monitored-system-cap expansion. Shared
helpers and route-owned browser symbols should name that commercial state as
plan selection as well; the monitored-system alias belongs only to
backward-compatible handoff normalization and non-commercial usage review.
If Pulse cannot create or resolve that portal handoff locally, the Pulse-owned
start route must still return the operator to the owned billing plan surface
with an explicit purchase=unavailable arrival instead of leaving the browser
on a raw error page, so the runtime can explain that Pulse Account is
temporarily unavailable and offer a retry from the same instance.
That portal handoff is now intentionally narrowly stateful rather than a bare
lookup row: lookup must stamp a first resolved_at, and the reported handoff
lifecycle must stay derived from the owned portal handoff plus the bound
checkout intent (created, resolved, checkout_started, completed) so
Pulse Account can distinguish a fresh upgrade bootstrap from a resumed or
already-completed checkout without reviving browser-owned commercial state.
That same owned handoff row is now also the canonical self-hosted purchase
binding record: it must persist the signed purchase_return_jti, the bound
Stripe session_id, and the lifecycle timestamps that prove when checkout was
resolved, started, and completed. Stripe success must carry that same
portal_handoff_id back into Pulse's activation callback, and Pulse must
cross-check both portal_handoff_id and purchase_return_jti against the
commercial session result before local activation so the browser no longer
trusts Stripe metadata or local form state alone for return integrity.
Once that commercial binding verifies, Pulse must not fall back to a generic
JTI replay tombstone. The local activation callback must persist a dedicated
purchase-return redemption record keyed by portal_handoff_id plus
purchase_return_jti, stamp explicit local redemption state
(started, activated, failed), and use that owned record to make
completed returns idempotent while still allowing retry after transient local
activation failures.
Stripe success now lands on Pulse's public
frontend-modern/src/utils/pricingHandoff.ts and
frontend-modern/src/pages/PricingHandoff.tsx may only hand operators into
Pulse-owned GET /auth/license-purchase-start; they must not construct
Pulse Account portal URLs, return_url parameters, or other portal-entry
contract state in the browser once the server owns that handoff boundary.
GET /auth/license-purchase-activate bridge, which auto-submits into the
owned POST activation path; the portal must not render a second manual
Activate in Plans & Billing step after checkout. Stripe cancel must return
directly to the owned Pulse billing plan route rather than back into the
portal. The owned activation callback must accept the signed state, redeem the
completed checkout, and return the operator to the canonical billing plan
route with an explicit owned purchase arrival state (purchase=activated,
purchase=expired, purchase=failed, or purchase=unavailable) whether
checkout completed in a secondary tab or in the current-tab fallback path.
When billing preserves intent=self_hosted_plan across that return, the
runtime may keep the operator on the compare-plans prompt, but any visible
Compare plans action must still route through owned
GET /auth/license-purchase-start?feature=self_hosted_plan instead of
constructing a direct Pulse Account URL in the browser.
That same purchase-return contract also narrows the insecure local-development
carve-out: signed purchase-return callbacks may use plain HTTP only for
loopback hosts such as localhost or 127.0.0.1. RFC1918, .local, and
other LAN-visible hosts must use HTTPS so the token-bearing return state is
not exposed to same-subnet sniffing during commercial checkout return.
That destination split is canonical commercial truth, but navigation semantics
are not owned here. frontend-modern/src/utils/pricingHandoff.ts and
frontend-modern/src/stores/license.ts decide which href each commercial
feature resolves to; frontend-primitives owns the typed navigation contract
that decides whether that href stays in-app or opens externally. Commercial
surfaces must not re-infer that behavior locally with per-component
target="_blank" or window.open(...) branches once a feature can resolve to
either product-owned billing/cloud routes or the public pricing site.
The self-hosted trial-start boundary is now retired for ordinary v6 GA
runtimes. Local POST /api/license/trial/start must not be registered as an
in-app acquisition route, and it must not return the old
hosted-signup or trial-rate-limit payloads from a normal self-hosted instance.
The retired /auth/trial-activate return path must also stay out of the
ordinary self-hosted router and Pro settings UI. Hosted/cloud entitlement lease
refresh may still validate signed leases for already-approved hosted state, but
ordinary self-hosted Pulse must not create a local trial acquisition callback.
The remaining TrialActivation* verifier names and
PULSE_TRIAL_ACTIVATION_PUBLIC_KEY environment literal are boundary-only
compatibility for hosted entitlement lease signing and already-deployed tenant
configuration. New entitlement runtime call sites must use the
HostedEntitlement* aliases unless they are explicitly proving the retired
callback boundary, and changing the environment literal requires a separately
governed credential rollout.
The matching control-plane acquisition routes are retired too:
/start-pro-trial, /trial-signup/*, and /api/trial-signup/* must remain
unregistered. /api/entitlements/refresh remains the only hosted entitlement
refresh endpoint for already-issued hosted leases.
Browser and shell coverage now guard that retired boundary:
tests/integration/tests/07-retired-trial-acquisition.spec.ts and
tests/integration/scripts/retired-trial-acquisition-contract.sh must expect 404 from the
retired route and prove entitlements remain unchanged. The paid-prompt browser
proof in tests/integration/tests/58-self-hosted-trial-rate-limit-ui.spec.ts
must keep trial CTAs and paid-only navigation out of the default self-hosted UI.
scripts/tests/test-retired-trial-acquisition-docs.sh guards the same documentation posture
so active operator docs and eval metadata describe the retired route instead of
the old hosted-signup acquisition contract.
Hosted tenant organization seeding and hosted handoff role mapping now belong
to the same cloud-paid truth too. internal/cloudcp/stripe/provisioner.go
must seed tenant org members from the shared account-role-to-organization-role
mapping, and hosted handoff/runtime consumers must preserve that same mapping
when older workspaces need membership continuity repaired at open time. Cloud,
MSP, and commercial account roles therefore have one canonical translation into
tenant org permissions instead of drifting between provisioning-time seeds and
handoff-time access repair.
The old self-hosted trial activation return notice and /settings/system-pro
route are retired with that callback: the trial query result must not produce
owned activation UI, a success banner, retry copy, or a settings compatibility
redirect in v6 GA. Purchase activation continues through the Pulse Account
return contract instead.
That same retirement applies to the old hosted control-plane completion and
redeem routes: /trial-signup/complete and /api/trial-signup/redeem must
not return customer-facing trial UX or raw acquisition errors in v6 GA.
Commercial plan disclosure copy must not be redefined inline in settings
feature gates, Pro license panels, or upgrade nudges.
That same disclosure copy must stay professional and customer-facing. The
settings ledger may expose counting details and included collection paths, but
it must avoid informal debug-style labels or ad hoc wording that makes the
commercial usage surface feel provisional.
That same governed ledger support surface now also owns backend-authored status
explanation copy beside the monitored-system status label. The frontend details
view may normalize a safe default when that field is absent during mixed-version
rollouts, but it must render the canonical backend explanation when present
instead of inventing page-local wording for what warning, offline, or unknown
means on a counted monitored system.
That same cloud-paid surface must now also render the canonical status reason
list when present, so customers can see exactly which grouped source or
top-level surface degraded and its canonical reported_at timestamp rather
than only reading generic status copy beside a fresh aggregate Last Seen
value.
That same settings surface must also label the monitored-system signal by its
real meaning. The canonical API shape is now the structured
latest_included_signal object, and the normalized frontend client contract
should expose only that canonical object. Retired flat alias fields must not
re-enter the live backend payload or client contract. It represents the freshest
included grouped observation, not a guarantee that every grouped source is
healthy, so the UI must not present it with single-source Last Seen wording.
When the canonical object is present, the surface should use its
source/name/type attribution instead of showing an unqualified aggregate
timestamp, and that attribution should stay customer-facing rather than
exposing raw monitored-system type/source slugs in the settings table or
included-surface details. When the row expands to show status reasoning, it
should also restate the freshest included surface and timestamp there so a
degraded reason and a fresher grouped signal remain readable together. When
mixed-version payloads omit that canonical freshest signal entirely, the
settings surface should degrade to a safe customer-facing
fallback instead of an unexplained placeholder glyph.
That same billing support boundary now also owns the shared monitored-system
presentation helper. frontend-modern/src/utils/monitoredSystemPresentation.ts
is the canonical owner for monitored-system brief/disclosure copy, ledger
labels, safe fallback summaries, source/type attribution wording, and the
customer-facing monitored-system usage/migration strings reused by the shared
settings and support ledger surfaces, so those surfaces must consume that
helper instead of redefining customer-facing monitored-system copy inline or
keeping a parallel copy in generic self-hosted plan utilities.
That same helper also owns preview-impact copy and current/projected
source-label wording for optional monitored-system impact previews, so the
TrueNAS and VMware settings panels do not drift into provider-local billing
phrasing when they surface canonical preview results. Impact preview summaries
must also come from that helper and describe current/projected count impact
without raw current / limit quota math.
That same helper-owned impact-preview contract now also owns unavailable
verification messages. Provider settings
panels must map monitored_system_usage_unavailable plus backend
details.reason through frontend-modern/src/utils/monitoredSystemPresentation.ts,
but those preview failures are explanatory only and must not disable or block
connection saves.
That same disclosure surface must not accept arbitrary caller-supplied
monitored-system summary strings as its primary API. When the disclosure needs
to show brief summary copy, it should render the canonical helper-owned brief
summary rather than giving callers a free-form summary prop that can drift from
the governed monitored-system language.
That same helper now also owns the row-status fallback summaries used when
mixed-version payloads omit status_explanation.summary. The API client and
settings panel must derive online/warning/offline/unknown fallback text from
that shared owner instead of keeping separate local defaults that can drift.
That same customer/support-facing ledger surface must consume the canonical
/api/license/monitored-system-ledger/explain model when showing counted
systems. Rows must expose backend-authored explanation.summary, reasons, and
grouped source surfaces through helper-owned labels so customers can see why a
system counts once without the panel inventing a second counting model.
When canonical usage is not safe to read yet and the backend returns
monitored_system_usage_unavailable, the support ledger must render
helper-owned verification copy instead of a synthetic count. The canonical
unavailable reason mapping belongs to
frontend-modern/src/utils/monitoredSystemPresentation.ts; Pro license panels
and ledger panels must consume that helper instead of rechecking availability
or hard-coding Verifying... / Unavailable formatting locally.
The entitlement payload builder must only mark monitored-system usage available
from an explicit canonical MonitoredSystemsAvailable signal supplied by the
runtime usage boundary. Deprecated compatibility aliases such as Nodes, or a
non-zero raw monitored-system count without that availability signal, must not
drive current usage, limit state, or customer-facing cap warnings.
The same usage view must not render monitored-system continuity context from
the entitlement payload as a live commercial cap. Base plan limits, effective
limits, grandfathered floors, and capture state are retired compatibility
metadata, not customer-facing self-hosted capacity controls.
beside the canonical count explanation.
That same settings owner split now also requires
frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx to
consume the normalized ledger client contract directly. Panel-local fallback
reconstruction for status_explanation, explanation, or
latest_included_signal belongs in
frontend-modern/src/api/monitoredSystemLedger.ts instead, so the billing
surface stays a pure render shell over one canonical monitored-system ledger
shape.
Frontend billing/admin surfaces must not synthesize plan_version from
subscription lifecycle state. When a hosted billing record lacks a plan label,
the UI must preserve that absence instead of fabricating values like active
or suspended into the canonical plan field.
The hosted billing-admin settings surface is now part of the explicit
cloud-paid ownership model as well. Changes to
frontend-modern/src/components/Settings/BillingAdminPanel.tsx must carry this
contract and the dedicated billing-admin proof file instead of remaining an
unowned consumer of hosted billing state.
That hosted billing-admin owner is now intentionally split by responsibility:
frontend-modern/src/components/Settings/BillingAdminPanel.tsx is the
settings shell, frontend-modern/src/components/Settings/useBillingAdminPanelState.ts
owns the hosted organization list, billing-state cache, preload, and
subscription update runtime, and
frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx
owns the tenant grid plus expanded JSON presentation. Future hosted billing
admin changes must extend that split instead of pulling hosted state and
mutation flow back into the panel render shell.
Hosted billing admin organization row actions follow the shared frontend
primitive contract as well: cloud-paid owns Suspend, Activate, Reload, tenant
billing state, and mutation semantics, while frontend-primitives owns the
Button chrome for those row actions.
The organization billing settings surface now follows the same rule. Changes
to frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx must
carry this contract and the dedicated organization-billing proof file instead
of remaining an unowned consumer of plan tier, entitlement limits, and
usage-versus-cap presentation.
That same billing surface now also normalizes org scope through
frontend-modern/src/utils/orgScope.ts before it builds per-tenant state, so
cache keys and tenant lookups do not keep a local getOrgID() || 'default'
fallback in the hosted billing UI.
That hosted organization billing owner is now intentionally split by role:
frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx is the
settings shell, frontend-modern/src/components/Settings/useOrganizationBillingPanelState.ts
owns org-scoped billing/runtime lifecycle and plan/usage model wiring, and
frontend-modern/src/components/Settings/OrganizationBillingLoadingState.tsx
owns the loading skeleton. Future billing work must extend that split instead
of pulling org-switch lifecycle and hosted entitlement reads back into the
panel render shell.
The self-hosted and hosted billing surfaces now also share a canonical
commercial page shell and plan/usage model. ProLicensePanel.tsx and
OrganizationBillingPanel.tsx may still differ in deployment-specific actions
and context, but CommercialBillingSections.tsx and
frontend-modern/src/utils/commercialBillingModel.ts now own the shared
commercial information architecture. Future billing work must extend that
shared shell/model first instead of letting self-hosted Pulse Pro and hosted
organization billing drift back into parallel local layouts or vocabularies.
For self-hosted Pulse Pro specifically, the plan/usage split is now also a
router contract: /settings/system/billing/plan is the canonical plan state,
and /settings/system/billing remains a compatibility handoff rather than the
primary owned destination. /settings/system/billing/usage is only a
compatibility support state for a displayable bounded monitored-system context
that remains after recognized self-hosted v6 plan labels have been normalized.
Community, Relay, Pro, Pro+, lifetime, and eligible grandfathered recurring
plans must not keep a standing Usage subtab alive just to restate counted-unit
totals or stale migration volume metadata; direct /usage arrivals on those
no-cap plans should canonicalize back to /plan. Arrival-specific UI
affordances belong to that same owned billing surface: legacy usage arrivals
may open counting rules by default only when a bounded support context is still
displayable, and plan arrivals may surface an upgrade callout that hands off
into Pulse Account for self-hosted plan comparison and checkout before
returning to the owned plan state through the runtime-owned activation
callback. Pulse product routes
keep ownership of license status, usage, and activation state; Pulse Account
owns the commerce flow itself. That same self-hosted settings surface is
plan-owned rather than tier-owned: the navigation label and surface title are
Plans & Billing, and the canonical plan state must make
the current tier plus capabilities immediately obvious so existing paid
upgrades can confirm what their new key enabled without hunting through
generic billing details. That capability-first summary must stay tier-specific
and continuity-aware: Relay and Pulse Pro should describe the actual paid
capabilities available on this instance, while grandfathered pricing must make
existing-price protection explicit without reviving finite monitored-system
continuity copy for normalized no-cap self-hosted plans.
That top card is plan-owned, not raw subscription-state-owned: Community copy
should describe what is included on this instance in plain language, and the
current-plan badge must not present the fallback Community state as Expired
just because a prior paid subscription or trial ended.
Upsell on the self-hosted Plans surface must stay contextual and subordinate
to current-plan clarity. The top card confirms the current tier first; only
below that may Pulse show a factual comparison section for the next higher
tiers. Community may compare Relay and Pulse Pro, Relay may compare Pulse Pro,
and active Pulse Pro should not show a promotional higher-tier block on this
surface.
Fresh activation is part of that same governed plan state. After a checkout
return, trial handoff, or pasted-key activation succeeds, the owned plan
surface must show an explicit success summary that names the active tier and
the marquee capabilities now available on this instance, rather than
falling back to a generic "activated" banner that leaves the user to infer
what changed from the steady-state billing card alone.
For active Pulse Pro/Lifetime tiers with the private Pro runtime available,
both the current-plan summary and the activation-success body must make the
next setup decision explicit: set Patrol mode, then let Patrol investigate,
act within that policy, verify outcomes, and keep context/history. Completed
verified Patrol outcomes are telemetry on this surface, not a reason to replace
the plan action with a default Patrol history CTA; when no active Patrol work
or pending governed decision exists, the paid-plan action returns to
Choose Patrol mode.
When the activated entitlement includes the governed Pulse Intelligence
operations-loop capability, that success summary may hand the user directly to
Patrol with a "Choose Patrol mode" action. The cloud-paid presentation contract
must name the action URL and action intent as Patrol-control concepts
(PATROL_CONTROL_STARTER_URL and patrol_control) rather than exposing
operations-loop names in the Plan surface. That action is still
cloud-paid-owned activation UX, but its destination must use the route-owned
Patrol control target
(/patrol?patrolControlStarter=patrol_control#patrol-control) so Patrol
owns starter recording through the shared Pulse Intelligence starter contract.
The legacy patrol_autonomy and pulse_pro_activation route and activity
markers remain compatibility input only, and the shared
pulse_operations_loop telemetry workflow marker may remain behind the Patrol
control surface. The Plans surface must not create a commercial-only analytics
counter, must not record prompt text or resource context, and must not treat the
click as proof that Patrol found an issue, Assistant explained it, a governed
action was approved, or the outcome was verified.
That same router-owned billing contract now also includes recovery as a plan
detail state instead of a fragment alias. The canonical recovery arrival is
/settings/system/billing/plan?details=recovery, while
#pulse-pro-recovery remains a compatibility deep link that must normalize
into that owned query state. purchase=failed arrival handling on the
self-hosted billing surface must consume the one-shot purchase result, preserve
the owned plan intent when present, and replace the URL with the canonical
recovery detail route so Open recovery lands on a first-class billing state
instead of a hash that settings-shell normalization strips away.
That same ownership split is explicit in the governed registry as well:
CommercialBillingSections.tsx is part of the shared commercial shell/model
surface, while SelfHostedCommercialRecoverySection.tsx stays on the
self-hosted Pro recovery surface with ProLicensePanel.tsx rather than
floating as an unowned settings fragment.
Hosted tenant browser bootstrap is part of that same cloud-paid boundary as
well. After control-plane or magic-link handoff, the browser client must
preserve the tenant-scoped pulse_org_id context that the server issued
instead of clearing it on first page load or collapsing back to default
simply because hosted tenants do not expose self-hosted multi-tenant admin
capabilities. Hosted runtime entry must therefore treat tenant org context as
infrastructure state, not a paid UI toggle that can be discarded during app
bootstrap.
That hosted browser bootstrap owner is now intentionally split by role:
frontend-modern/src/App.tsx is the route/provider entry shell,
frontend-modern/src/useAppRuntimeState.ts owns authentication, org bootstrap,
theme synchronization, and authenticated runtime startup, and
frontend-modern/src/AppLayout.tsx owns authenticated cloud-aware chrome such
as org switching and kiosk-safe navigation. Future hosted browser bootstrap
work must extend that split rather than pulling org bootstrap and app chrome
back into one monolithic route component.
That same authenticated shell split must also respect shared blocking dialogs:
hosted chrome may not leave the Pulse Assistant launcher or an already-open
assistant drawer interactive behind a modal that currently owns the viewport.
That same hosted chrome rule also covers responsive launcher placement. While
the authenticated shell is using mobile navigation, frontend-modern/src/AppLayout.tsx
must keep the closed Pulse Assistant launcher as a bottom floating affordance
that clears the mobile nav; it must not reintroduce the desktop right-edge
launcher until the desktop shell breakpoint, otherwise hosted and self-hosted
operators lose usable dashboard and billing-page edge space at tablet widths.
That same authenticated shell contract now also has to distinguish backend
availability from websocket-stream liveness. When hosted runtime health stays
available during a stream reconnect or renegotiation window,
frontend-modern/src/useAppRuntimeState.ts must preserve a canonical
backend-healthy signal and frontend-modern/src/AppLayout.tsx must not
advertise the whole paid shell as disconnected or "Reconnecting..." solely
because the live stream is transiently recovering. Future hosted shell work
must keep the top-right connection badge aligned to overall authenticated
runtime availability first, with websocket churn treated as a narrower live
stream status instead of the shell-level truth.
That same hosted shell badge must also stay explicit about degraded-but-usable
paid runtime states. When the backend remains healthy but the live stream is
recovering, the authenticated shell should surface a degraded sync state
instead of collapsing back to the same disconnected wording used for total
runtime loss, so hosted operators can tell the difference between browser/API
usability and live-stream freshness at a glance.
That same authenticated shell contract also applies to every chrome consumer of
runtime health, not only the badge label. frontend-modern/src/AppLayout.tsx
must consume the canonical connectionStatus contract end-to-end for shell
behavior such as the Pulse logo activity animation rather than retaining
parallel connected/reconnecting prop assumptions after the runtime model
moves. Future hosted shell changes must update all chrome affordances together
so a status-model refactor cannot leave stale prop calls behind that crash the
authenticated cloud shell at render time.
That same route/provider shell must stay page-oriented as well: where a
browser route exists, App.tsx should lazy-load the route shell instead of
wiring product-surface components such as
frontend-modern/src/components/Storage/Storage.tsx directly into the router.
Retired aggregate or utility aliases must stay unregistered rather than being
kept as route-shell compatibility handoffs, so hosted bootstrap ownership stays
at the app boundary rather than leaking route concerns back into feature
components.
That same authenticated route shell also defers to the
frontend-primitives-owned post-auth landing path. frontend-modern/src/App.tsx
and frontend-modern/src/pages/RuntimeHome.tsx must send authenticated /
through the runtime-home landing contract first; hosted workspaces with no
connected infrastructure may still fall through to the single
/settings/infrastructure ownership path, but the shell must not regress into
a root-only settings redirect, legacy Infrastructure default, or
dashboard-only shortcut that strands first-time hosted tenants.
That same landing contract also owns authenticated /login: once the browser
has a valid session, frontend-modern/src/App.tsx must route /login
through that same runtime-home landing boundary instead of leaving the
authenticated app shell on a not-found compatibility path.
Authenticated app-shell route and chart prewarming may run from
frontend-modern/src/App.tsx and frontend-modern/src/useAppRuntimeState.ts
only as auth-neutral performance work: it must not change hosted organization
bootstrap, presentation-policy loading, commercial posture loading, or upgrade
prompt visibility. The runtime bootstrap must not prewarm retired
Infrastructure or Workloads summary-chart caches as a generic authenticated
side effect; route-module warming stays in App.tsx, and chart payloads belong
to the route or interaction that renders them.
The Pro license settings surface now follows the same rule as well. Changes to
frontend-modern/src/components/Settings/ProLicensePanel.tsx must carry this
contract and the dedicated Pro-license proof file instead of remaining an
unowned consumer of activation, trial eligibility, entitlement capability, and
plan-term presentation.
That owned Pro-license boundary is now intentionally split by role:
frontend-modern/src/components/Settings/ProLicensePanel.tsx is the settings
shell, frontend-modern/src/components/Settings/useProLicensePanelState.ts
owns activation, trial, route-scoped notices, and commercial plan runtime
state, and frontend-modern/src/components/Settings/ProLicensePlanSection.tsx
owns the plan/read-model presentation. Future Pro-license work must extend
that split instead of pulling route state, entitlement derivation, and retry
lifecycle back into the shell component.
That owned presentation boundary includes the settings shell itself: the
top-level self-hosted billing surface must keep its page-shell title and
leading SettingsPanel title aligned on the canonical Plans & Billing label
so commercial activation, trial, and pricing state do not present as one
surface in navigation and a differently named surface in the actual paid
settings UI.
That same boundary also owns the shell and section framing copy for the
self-hosted Pro billing surface. ProLicensePanel.tsx must consume shared
presentation for its shell title/description, refresh CTA label, and section
headings rather than carrying those commercial labels inline.
The same title should also feed the system-billing settings navigation item,
and the same title and description should also feed the system-billing route
header metadata so the nav, shell, and route header do not narrate the same
commercial surface differently.
That same shared presentation owner also carries the canonical cross-surface
referral copy used outside the billing shell itself. When infrastructure or
other adjacent settings surfaces need to point operators toward Plans & Billing
for billing, license status, or paid feature access, they must consume the
settings-owned referral strings from
frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts
instead of drafting route-local commercial guidance or reaching directly into
generic commercial helpers from hosted settings routes.
That same hosted-settings presentation boundary is explicit about bundle
ownership. frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts
is the canonical settings-shell adapter for self-hosted Plans & Billing shell
framing and referral copy, while
frontend-modern/src/utils/licensePresentation.ts remains the shared
commercial notice/label owner for activation, trial, purchase, and recovery
language such as License key. Hosted settings surfaces must
not import self-hosted billing framing straight from the generic helper module
when doing so would reintroduce top-level bundle-init cycles into hosted
tenant settings routes.
Paid Pulse Pro v5 grandfathering is now part of that same canonical boundary:
when a recurring v5 customer migrates into v6, billing persistence,
entitlement evaluation, renewal handling, and Pro-license presentation must
preserve the customer's existing recurring price identity and uncapped
commercial capacity instead of silently rewriting them onto current v6 retail
pricing or Pro-era caps. Activation persistence and grant refresh may still
carry local-only legacy-migration continuity metadata as a defensive fallback
for any bounded legacy grant that survives outside the canonical v5 recurring
contracts, but active recurring v5/v1 customers must not rely on a captured
floor to stay admissible in v6.
That fallback continuity path is scrub-owned rather than read-owned. Ordinary
status or entitlement reads may tolerate old continuity fields while loading
legacy records, but they must not persist grandfather floors, start capacity
reconciliation, or expose monitored-system continuity as live commercial state.
Save-time monitored-system commercial denials are retired. TrueNAS and VMware
settings may render explicit preview results for grouping impact, but rejected
saves must flow through ordinary API error handling rather than a preserved
monitored-system cap preview.
That continuity rule cannot depend on webhook metadata being perfect. The
canonical Stripe price-to-plan lookup in pkg/licensing/features.go and
pkg/licensing/stripe_subscription.go must recognize the still-renewing
grandfathered recurring v5 and legacy v1 Stripe price IDs directly, so a real
customer.subscription.updated event that omits metadata.plan_version still
resolves to v5_pro_monthly_grandfathered or
v5_pro_annual_grandfathered instead of falling back to an opaque
stripe_price:* marker.
That Pro-license presentation rule is explicit UX, not only hidden metadata:
when a migrated recurring v5 plan is active or in grace, the settings surface
must render plan terms and a continuity notice that makes it clear the
existing recurring price remains in force until cancellation, while
self-hosted monitoring and child-resource volume are not metered in current v6
self-hosted packaging. The same plan summary must not render a separate
Guest Capacity, child-resource allowance, or equivalent volume-cap row for
uncapped/grandfathered self-hosted plans; the customer-facing continuity story
is existing-price protection plus unmetered self-hosted monitoring and
child-resource volume in current v6 self-hosted packaging, not a new paid
guest-capacity benefit.
The self-hosted commercial presentation on that same surface is now locked to
the no-cap monitored-system model as well. ProLicensePanel.tsx,
CommercialBillingSections.tsx, and
frontend-modern/src/utils/commercialBillingModel.ts must present current v6
self-hosted packages as core monitoring included in every self-hosted tier plus
plan-specific extras: Community stays free for core monitoring, Relay adds
remote access/Pulse Mobile handoff/push convenience and 14-day history, Pro adds Relay plus
hands-on Patrol modes, issue investigation, verified fixes, advanced administration,
and 90-day history, while Pro+ remains legacy continuity only.
Cloud/MSP pricing semantics stay separate, and grandfathered v5 continuity copy
must describe legacy continuity and recorded baselines rather than current
self-hosted monitored-system caps, capacity, or policy boundaries.
Active Relay and Pro installs may also render a factual value-proof panel on
the owned Settings -> Plan surface, but that proof must derive from the live
self-hosted entitlement payload rather than from public pricing copy. It may
render only for active/grace/trial paid self-hosted states with a numeric
max_history_days payload, must suppress itself for Community, inactive, and
missing-payload states, and must describe partial or missing rows as
activation/recovery attention rather than as marketing pressure. Relay proof is
limited to remote access, Pulse Mobile pairing for handoff, push notifications, and the
reported 14-day history entitlement. Pro proof adds the reported 90-day history
entitlement plus root-cause/remediation and team/admin capability groups.
Frontend presentation and component tests must cover active, partial, and
Community-suppressed proof states whenever this panel changes.
That same settings-owned presentation must distinguish active commercial
features from stale bounded legacy fallback metadata. Active grandfathered
recurring v5 plans must render the existing recurring price continuity
directly and must not show a pending or captured floor banner or any finite
monitored-system volume cap. Recognized v6 plan labels must follow the same
customer-facing no-cap rule even if a legacy entitlement still carries
plan_limit, effective_limit, grandfathered_floor, or capture-pending
telemetry: useProLicensePanelState.ts must suppress Usage tabs,
monitored-system policy sections, continuity notices, plan-limit detail rows,
and pause-new-admissions copy, while licensePresentation.ts must keep current
plan summaries focused on core monitoring plus the actual tier extras.
When billing or support presentation still needs a capacity-free self-hosted
summary value, the customer-facing label is Not metered, not Unlimited;
lifetime commercial continuity duration may use Permanent, but no normal
self-hosted plan surface should imply monitored-system volume is a paid tier.
That same pricing boundary now also owns the shared frontend plan-definition
models. frontend-modern/src/utils/cloudPlans.ts and
frontend-modern/src/utils/selfHostedPlans.ts are the canonical frontend
sources for cloud and self-hosted plan copy, limits, and comparison metadata.
frontend-modern/src/pages/CloudPricing.tsx,
frontend-modern/src/pages/HostedSignup.tsx, and the self-hosted billing
settings surfaces must consume those shared plan-definition sources instead of
redefining retail plan facts or counted-unit policy locally.
That shared ownership also includes display-ready price semantics. Monthly
headline price, founding-rate override, compare-at strike-through copy,
campaign badge copy, and annual summary text must come from the shared
plan-definition owners rather than page-local string parsing or hardcoded
retail amounts inside hosted pricing/signup screens.
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 commercial framing inside product-owned
billing and activation surfaces: plan names, counted-unit copy, and upgrade
adjacency must come from the shared self-hosted plan-definition owner rather
than page-local strings. That owner must also keep Community, Relay, and Pro
framed as core monitoring included in every self-hosted tier plus their
bundle-specific extras rather than drifting back to bounded monitored-system
sales copy, vague systems, or device-style language. The same shared owner
must also provide the billing surface bundle summary and metric-history facts
for current unmetered self-hosted tiers, so commercialBillingModel.ts and
useProLicensePanelState.ts do not regress back to legacy guest-capacity or
meter-style entitlement fields on retail Community, Relay, or Pro plans. Pro+
may appear only as a legacy continuity tier, and
frontend-modern/src/utils/licensePresentation.ts must label pro_plus
accordingly in tier, plan, and plan-version presentation instead of rendering
it as a current public Pulse Pro+ package. When direct plan-selection intent
opens the explicit self-hosted comparison surface, the shared presentation
helpers must show Pro's operations, admin, and reporting extras together while
still framing Community, Relay, and Pro as core monitoring included in every
self-hosted tier rather than monitored-system allowance tiers. Relay copy in
that shared owner must describe the current v6 GA product as standard Relay
remote access, supported Pulse Mobile pairing for handoff, push notifications, and 14-day
history, and it must not market a customer-specific Relay URL until that
routing surface exists as a backed product capability. Cloud common-inclusion
copy follows the same mobile-language rule: describe supported Pulse Mobile
pairing for handoff and push notifications rather than generic mobile app access. The same factual
plan surface must keep inactive-state copy neutral: default Community installs
must describe the instance as ready to use rather than as missing an activation
key, and they must not foreground paid self-hosted activation, v5-era "No Pro
license" language, or unlocked entitlement phrasing before the operator opens
an explicit comparison, checkout, activation, or recovery path.
The shared license presentation owner also holds self-hosted Pro settings
trial-ended notice copy for ProLicensePlanSection.tsx; that surface must
consume canonical helper notices instead of carrying inline upgrade copy or
local status-tone branches.
That same frontend-modern/src/utils/licensePresentation.ts owner must treat
compatibility-only capability keys such as kubernetes_ai as non-marketed
technical facts: API/docs surfaces may still name the compatibility route, but
customer-facing self-hosted current-plan summaries and unlocked-capability
lists must not surface those keys as marquee Pro value. MSP-only contract keys
such as multi_tenant follow the same rule inside the self-hosted Pro plan
surface: they may remain entitlement inputs for hosted/MSP/account surfaces,
but they are not self-hosted Pro value copy. The same rule applies to legacy
claims such as incident memory: current v6 upgrade notices and commercial
copy must use the canonical 90-day history framing until a distinct
incident-memory product exists.
That same plan-section boundary must also defer notice resolution to component
runtime. frontend-modern/src/components/Settings/ProLicensePlanSection.tsx
may not compute trial-ended notices at module scope, because hosted settings
bundles must survive top-level import ordering without throwing
initialization-time ReferenceError crashes before the workspace UI mounts.
That same plan-section boundary also owns the rule that Community (non-paid)
users must not be funneled through upgrade CTAs inside Settings -> Plan.
ProLicensePlanSection.tsx must not render the monitored-system upgrade
arrival banner, the trial-start CTA, or the inactive-Pro upsell notice to
users without paid features, licensePresentation.ts must not retain a dead
inactive-Pro upsell helper for that surface, and the capacity section must not
render an upgrade-plan button for monitored-system pressure. Self-hosted Pro marketing lives at
pulserelay.pro/pricing; the Settings plan surface must show factual
license state for Community users and leave discovery of paid tiers to
surfaces outside the plan page.
That same no-funnel rule extends beyond Settings -> Plan. The self-hosted
frontend must not render blanket trial/upgrade marketing to Community users
from the main app shell, the setup wizard completion screen, or any
app-wide surface that fires without the user having explicitly engaged with
a paid feature. In particular the retired Dashboard overview must not be
restored to carry a RelayOnboardingCard paywall or equivalent Start trial
prompt, the
SetupCompletionPanel may not carry a Monitor from Anywhere Relay trial
block, and no time-triggered "active use" nudge such as ActiveUseTrialNudge
may auto-appear for Community users. Feature-gated discovery that fires only
when a Community user clicks a locked feature (for example alert
investigation, 30/90-day history ranges, Patrol AI autonomy modes, or
Settings panels whose feature the user opened themselves) remains in scope
for that feature's owning subsystem — those are user-initiated discovery
paths, not blanket funnels, and are not required to be removed.
Public AI and entitlement docs must use the same boundary: Community/Relay may
describe Patrol background findings with BYOK, while investigation, proposed
fixes, governed fix execution, and higher autonomy remain paid AI-operations
features.
Those docs should describe moving between available modes, not tell readers to
"upgrade" as part of an ordinary safety progression.
That same retired counted-unit boundary also owns the disclosure rule for
retail copy: default billing and pricing surfaces should not use
monitored-system allowance copy. Monitored-system grouping definitions belong
only behind explicit support/ledger disclosures rather than persistent
plan-tab chrome.
The same boundary also owns where monitored-system continuity truth lives. A
dedicated self-hosted Pro plan-surface continuity section is only canonical when
Pulse is reconciling explicit carry-forward support context outside normal
self-hosted monitoring. Normal self-hosted plans must not keep a Monitoring capacity section alive just to restate that monitoring is included without a
monitored-system volume gate; counted-unit explanation plus current usage
inspection belongs to the ledger/disclosure path, not to plan-capacity UI.
The app shell must not expose a monitored-system warning entry point for
self-hosted plan volume. Support review states can link to the usage-owned
ledger, but the CTA must not revive "View capacity" copy as an upsell-shaped
monitored-system prompt.
Community overflow/setup-slot messaging must still explain the current
top-level monitored systems plus any temporary setup slot in customer terms
rather than compressing the contract into slash-style quota strings that imply
Pulse is counting every child device.
That same billing-facing boundary also owns why an installation can require
legacy review at all. When the monitored-system posture is over_limit_frozen,
customer copy must explain whether a carried-forward baseline needs review or
whether migrated v5 continuity capture is still pending. Billing surfaces must
not render an unexplained 15 monitored, plan includes 5 state that looks like
Pulse is enforcing a current self-hosted cap model.
Those same billing-facing surfaces must also describe the commercial contract in
customer terms: monitored systems, legacy continuity baselines, subscription
status, and license status. They must not revive legacy installed-agent
wording or vague internal nouns like allocation once the monitored-system
billing model is the canonical product truth.
Loading, empty, and temporary-unavailable states on monitored-system usage
surfaces follow that same rule: they should use calm customer-facing status
language such as usage loading or usage unavailable with a clear retry action,
not raw transport phrasing like failed to load.
The self-hosted pricing page should therefore stay focused on plan cards,
upgrade paths, and the comparison table rather than rendering a separate
counted-unit explainer card beneath the tier grid.
Hosted commercial pages follow the same presentation rule: CloudPricing.tsx
and HostedSignup.tsx should describe plan scope and setup in concise product
language, not in implementation-detail terms such as internal workspace
provisioning guarantees, control-plane routing mechanics, or narrated
deployment internals.
Those same hosted surfaces should also prefer neutral plan-selection and
sign-in language such as Choose Starter or sign-in link rather than
marketing-heavy CTA copy or infrastructure-specific transport jargon.
They should also avoid duplicating shared commercial facts across multiple
adjacent blocks on the same screen. Common Cloud inclusions belong in one
shared area rather than being restated inside every plan card, and hosted
signup should not repeat the selected plan facts across the page header, form
card, and plan summary at the same time.
That contract applies to both plan summary labels and upgrade/paywall copy:
current v6 self-hosted pricing may not drift back to the older $49/yr Relay,
$99/yr Pro, monitored-system-count marketing drift, or public Pro+ sales
language that contradicts the locked Community / Relay / Pro no-cap packaging.
Cancellation is the explicit boundary for that policy. Once a grandfathered v5
recurring subscription is canceled, any later return must resolve through the
current v6 pricing contract rather than reviving the legacy recurring rate.
The canonical cross-repo manual drill for that boundary is
docs/release-control/v6/COMMERCIAL_CANCELLATION_REACTIVATION_E2E_TEST_PLAN.md.
The paid relay settings surface is part of that same ownership model. Changes
to frontend-modern/src/components/Settings/RelaySettingsPanel.tsx must carry
this contract and the dedicated relay frontend proof files instead of
remaining unowned consumers of relay licensing state.
Its paywall and activation copy must say Relay and higher plans rather than
Pro-only wording; Pro may include Relay, but Relay is still its own public
self-hosted tier.
That relay settings owner is intentionally split by role:
frontend-modern/src/components/Settings/RelaySettingsPanel.tsx is the
settings shell, frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts
owns relay config/status polling and pairing runtime, and
frontend-modern/src/components/Settings/RelayPairingSection.tsx owns the QR
pairing surface. Future relay settings work must extend that split instead of
pulling polling and QR-generation lifecycle back into the shell component.
The retired Dashboard shell must not be restored to host a Relay onboarding
card or equivalent blanket Relay upsell. Relay discovery belongs to the owning
Settings surface above; Infrastructure and Workloads stay monitoring-first
views with no app-wide paywall onboarding composed into them.
That relay pairing boundary now also includes backend-owned mobile credential
lifecycle: when the settings surface generates a mobile pairing QR, it must ask
the server for a fresh scoped Pulse Mobile relay access token, fetch the
onboarding payload through that token context, and tear down superseded or
failed unused tokens instead of accumulating hidden credentials.
That same pairing fetch path must stay token-bound instead of ambient-session
bound: the onboarding QR request must carry the freshly minted relay pairing
token explicitly so the returned payload reflects the exact credential that the
mobile device will bootstrap with, not whichever browser session happened to
open the settings page.
The same flow must fail closed if the pairing payload omits the authenticated
relay token state needed by the mobile deep link; pairing UI cannot silently
render a QR that bypasses governed auth ownership.
Relay pairing token presentation is part of that same contract as well: the
settings surface must label those Pulse Mobile relay access credentials
distinctly from long-lived automation tokens so operators can identify and
revoke stale mobile devices or abandoned pairing attempts without guessing
which credential was created for mobile runtime access.
That same runtime contract now uses a dedicated backend-owned
relay:mobile:access scope, with the server preserving backward-compatible
gates for older mobile tokens that still carry the legacy AI scopes during the
post-RC migration window.
Relay pairing refresh behavior is part of that lifecycle contract too: a
successful QR refresh must revoke the superseded token once the new token-backed
payload is ready only if the superseded token still shows no device use, while
a failed refresh must revoke only the new failed token and keep the previously
valid QR visible instead of collapsing the operator back to an empty pairing
state.
Hosted signup provisioning now follows the same rule. Changes to
internal/api/public_signup_handlers.go and internal/hosted/provisioner.go
must carry this contract and the dedicated hosted-signup provisioning proof
files instead of remaining a split boundary between API handlers and an
unowned hosted runtime helper.
That hosted signup boundary is now also canonical in shape: the public signup
handler owns request validation, trial billing initialization, and magic-link
issuance, while internal/hosted/provisioner.go owns the shared org
bootstrap/admin-role assignment, duplicate-owner-email idempotency, and
rollback path for hosted signup failures.
The same boundary is identity-stable: internal/hosted/provisioner.go must
generate a stable opaque owner user ID for new hosted signup organizations,
store the signup email only as OwnerEmail/member contact metadata, assign
RBAC against the stable owner ID, and return contact email separately so the
public signup handler can issue magic links without turning email into the
tenant principal.
That duplicate-owner-email rule is fail-closed and server-owned. Repeated
hosted signup attempts for an email that already owns a tenant must resolve to
the existing org identity instead of minting a second tenant and relying on
later auth or billing surfaces to untangle the collision.
That same public signup boundary must also stay privacy-safe at the browser
edge: syntactically valid signup requests return one uniform 202 Accepted
Pulse Account message whether the backend provisioned/sent email or suppressed
side effects due to the owner-email rate limit, so the route does not reveal
prior email usage through 201 versus 429 drift.
Hosted billing-state normalization now follows the same rule: a missing
plan_version must remain missing instead of being synthesized from
subscription_state, while explicit trial defaults remain explicit.
Hosted trial bootstrap and hosted entitlement refresh must not mint new
quickstart inventory. The pulse-pro license server route/config/proxy layer
for hosted quickstart is retired for v6 GA; historical quickstart fields may
remain parseable as persisted billing compatibility while those fields exist,
but new user-facing hosted or self-hosted acquisition work must not depend on
that inventory as a product promise, and lease-refresh rewrites must avoid
turning legacy fields into a renewed customer-facing hosted-model offer.
Hosted AI runtime defaults are part of the same boundary as well: when a cloud
tenant falls back to provider defaults, the persisted model identifier must
remain canonical provider:model data rather than a bare provider-local alias,
so hosted enterprise runtime startup does not fail before chat or approvals can
initialize.
Hosted release builds must not reopen the trial-activation public key through
runtime environment just because PULSE_HOSTED_MODE=true. Managed tenants may
still receive a hosted-specific verification key, but the release binary must
consume that key through the build-time embedded source of truth rather than a
runtime env override that can silently replace the trusted verifier after
deployment.
Legacy MSP plan aliases are input-only compatibility shims. Live runtime
defaults, fallback provisioning, entitlement issuance, and limit/workspace
lookups must resolve to canonical msp_starter rather than preserving
msp_hosted_v1 as an active first-class plan name.
Hosted control-plane plan resolution is now part of the enforced ownership
model: changes to hosted entitlement issuance, control-plane registry
canonicalization, or Stripe provisioning plan resolution must carry this
contract and the path-specific proof files that verify those boundaries.
Hosted tenant container bootstrap is part of that same boundary as well: the
control plane may bind-mount billing and handoff files into /etc/pulse as
read-only inputs, but runtime startup ownership repair must treat those paths
as immutable and skip chown attempts against them instead of aborting tenant
provisioning.
That startup repair boundary also treats image-owned runtime paths as
immutable. The container entrypoint may repair writable tenant data paths, but
it must not recursively chown built image paths such as /app or
/opt/pulse, because doing so copy-ups the runtime image into every hosted
tenant writable layer and turns fleet reconciliation into host-disk pressure.
That same immutable-file boundary now also owns write-time runtime ownership:
control-plane provisioning and later billing-state rewrites must leave
billing.json, secrets/handoff.key, and .cloud_handoff_key readable by
the tenant Pulse runtime user at the moment they are written. Fixing startup
chown behavior alone is not enough if the mounted files stay root:root
after provisioning, because hosted auth handoff and hosted lease reads will
then fail closed inside an otherwise healthy tenant.
Hosted tenant org bootstrap is part of that same runtime boundary. Cloud and
MSP tenant provisioning must seed a canonical tenant-scoped org.json under
orgs/<tenant-id>/ and leave both the directory tree and file readable by the
hosted runtime user; otherwise hosted magic-link handoff can preserve the
correct tenant org cookie while the tenant API still fails closed with
invalid_org because no tenant organization metadata exists on disk.
That same shared browser client boundary also owns structured error extraction
for hosted tenant requests. Cloud and MSP surfaces may show failures from
frontend-modern/src/utils/apiClient.ts, but they must resolve canonical JSON
error / message fields before showing UI feedback instead of leaking raw
response payloads while tenant-scoped org headers are still in flight.
That same boundary now also owns stable structured error metadata on the shared
browser client. When backend routes return canonical JSON code plus
string-valued details, frontend-modern/src/utils/apiClient.ts must preserve
that metadata on the thrown error object instead of collapsing everything to one
display string. Hosted and platform-onboarding surfaces may then classify
supported failure modes from the shared error object, but they must not invent
route-local parsing rules or provider-local transport shims to recover metadata
the shared client dropped.
Hosted tenant runtime env is part of the same contract too: provisioned
containers must carry hosted-safe tenant context such as
PULSE_TENANT_ID=<tenant-id>, PULSE_MULTI_TENANT_ENABLED=true, and an explicit
PULSE_PUBLIC_URL=https://<tenant-id>.<base-domain> so the tenant-scoped org
surface is actually enabled after handoff instead of failing closed under a
paid hosted session.
Hosted MSP workspace org seeding is part of that same boundary too:
internal/cloudcp/account/tenant_handlers.go owns the authenticated
account-scoped workspace-create entry path, and
internal/cloudcp/stripe/provisioner.go owns the underlying workspace/org
bootstrap. When the control plane provisions a new workspace under an existing
account, that boundary must seed org.OwnerUserID from the authenticated
creator email when that actor is known on the request path instead of
inferring a canonical owner from membership query order. If the creator is not
available, fallback owner selection must still be deterministic rather than
depending on newest-row ordering in the registry.
Hosted tenant entitlement evaluation is part of that same boundary too: when a
hosted tenant lands in a tenant-scoped org like t-..., the runtime must
still honor the instance-level hosted billing lease in default until or
unless an org-local billing state exists, rather than collapsing a freshly
provisioned paid tenant into subscription_required on first entry.
JWT-backed entitlement claim evaluation and activation-grant translation now
follow the same explicit proof model instead of relying only on the broad cloud
runtime catch-all policy.
Persisted billing-state normalization, hosted database-source loading, Stripe
plan derivation, and the cloud plan/limit tables now follow the same ratchet:
they are expected to move behind path-specific proof routes rather than staying
indistinguishable inside the generic cloud runtime policy.
The runtime entitlement surface now follows the same rule: evaluator/token
source accessors, hosted-subscription validity rules, and frontend entitlement
payload construction should move behind explicit proof routes rather than being
implicitly trusted as part of the catch-all cloud runtime layer.
That same runtime surface must stay internally coherent in local dev mode. If
pkg/licensing/service.go widens backend HasFeature() checks under
PULSE_DEV=true or demo/mock mode, the entitlement payload and any other
frontend-facing capability contract derived from that service must advertise
the same capability set instead of leaving paid surfaces artificially locked
behind stale free-tier capabilities.
That dev widening is still bounded by runtime readiness rather than marketing
intent: multi_tenant is not a valid advertised dev capability unless the
process also has PULSE_MULTI_TENANT_ENABLED=true, because the org API surface
is intentionally disabled otherwise.
The same runtime-readiness rule also excludes placeholder and plan-marker
entries like white_label, multi_user, and unlimited from dev/demo
entitlement payloads when there is no corresponding operable runtime surface;
those belong in tier metadata and plan semantics, not in live capability
advertising for a free dev shell. The unlimited feature key is a hosted/MSP
capacity-policy marker and must use neutral hosted-capacity wording in shared
metadata rather than customer-facing "Unlimited Instances" copy that implies a
self-hosted monitoring-volume tier.
The same distinction applies to customer-facing self-hosted billing
presentation. frontend-modern/src/components/Settings/useProLicensePanelState.ts
and frontend-modern/src/utils/licensePresentation.ts may label operable
capabilities, but they must not render placeholder or plan-marker entries like
white_label, multi_user, multi_tenant, or unlimited in the Pro plan
feature list just because those keys exist in tier metadata.
Cloud/MSP live price IDs are no longer an open fill-in task either. The audit
record docs/release-control/v6/records/cloud-msp-price-audit-2026-03-13.md
verified that the 13 canonical Cloud/MSP v6 price_* IDs are present in the
governed pulse-pro operational docs and license-server env template, and that
each ID resolves to an active live recurring Stripe price object.
Activation service runtime, license-server transport, encrypted activation
persistence, and hosted entitlement lease signing now follow the same ratchet. Changes
to pkg/licensing/service.go, pkg/licensing/grant_refresh.go,
pkg/licensing/revocation_poll.go, pkg/licensing/license_server_client.go,
pkg/licensing/persistence.go, pkg/licensing/activation_store.go, and
pkg/licensing/trial_activation.go should carry their dedicated proof files
instead of relying only on the generic cloud runtime policy.
That same activation-service boundary also owns test-only helper exclusion from
release builds: legacy JWT fixtures and grant JWT generators used by tests must
live in non-release build-tagged helpers instead of shipping inside the primary
runtime service file.
The remaining cloud-paid runtime families now follow the same rule as well:
feature/limit primitives, billing and entitlement type shapes, commercial
migration and trial flow, host lifecycle tracking, and public-key/build-mode
boundaries should all resolve through explicit proof routes rather than a
package-wide pkg/licensing/ fallback.
The retired conversion-telemetry boundary now keeps self-hosted commercial
progression out of customer-side browser analytics and out of the compiled
normal licensing package. Customer frontend source must not retain
upgradeMetrics, conversionEvents, infrastructure onboarding metrics
wrappers, or their call sites. The browser app must not emit local
pricing_viewed, checkout_clicked, paywall, commercial funnel, or onboarding
events from the in-app Plans & Billing plan surface.
pulse-pro:license-server/v6_checkout.go owns the Pulse Account handoff
equivalents bound to portal_handoff_id and the canonical checkout intent.
The browser app must not try to recreate those Pulse Account stages from
referrer state, and the commercial service must not collapse self-hosted v6
handoffs back onto the public-site release track when production public GA is
still on v5.
That same retired local conversion store is no longer a normal product read
model. /api/diagnostics and the Settings support diagnostics panel must not
expose pricing, checkout, conversion, or infrastructure-onboarding analytics,
and the normal API router must not register local commercial metric stats,
health, config, ingestion, or funnel reads. Maintainer commercial reporting
must live outside the self-hosted customer product runtime instead of reusing
the Pulse settings or diagnostics surface.
Stripe checkout and subscription webhook persistence now also follows the
retired monitored-system-volume rule: when paid state is granted, billing-state
writes must persist canonical plan and feature state while scrubbing retired
max_monitored_systems limits, and when paid state is revoked they must not
preserve stale paid capacity.
Grandfathered recurring v5/v1 continuity is the explicit stored-state
exception inside that same boundary. pkg/licensing/billing_state_normalization.go
must persist the canonical billing baseline for recognized grandfathered
recurring Stripe plans even though the live entitlement contract is uncapped,
so webhook persistence, hosted billing state, and admin inspection stay
deterministic instead of leaking an internal 0 == unlimited convention into
saved billing records. pkg/licensing/database_source.go,
pkg/licensing/models.go, and downstream entitlement evaluation must then
strip that stored monitored-system cap back out before any runtime entitlement
payload, so continuous grandfathered recurring customers stay uncapped until
cancellation.
That same monitored-system entitlement boundary also owns the retired-warning
copy rule: customer-facing surfaces must not revive the old limit banner,
migration freeze guidance, or agent-install quota language while describing
non-counted legacy/API-connected resources.
That same webhook boundary now also owns request-lifetime decoupling for
checkout provisioning: long-running checkout.session.completed tenant
provisioning must complete under an explicit background timeout instead of
depending on the inbound Stripe request context surviving long enough for first
boot and health polling.
That same Stripe webhook boundary also owns bounded idempotency retention in
the control-plane registry. internal/cloudcp/registry/registry.go may keep
dedupe rows long enough to suppress duplicate Stripe deliveries and reclaim
stale in-flight attempts, but it must prune expired processed or abandoned
stripe_events rows instead of letting webhook idempotency state grow without
bound on disk.
That same paid relay onboarding boundary now also owns QR-token lifecycle on
the supported Pulse Mobile pairing surface. RelaySettingsPanel.tsx,
RelayPairingSection.tsx, and useRelaySettingsPanelState.ts may revoke a
displayed pairing token only when canonical token metadata still shows no
lastUsedAt; once a token has been used by a paired device, refreshing or
hiding the QR must preserve that credential instead of treating it as
disposable UI state.
That same paid relay onboarding boundary also owns the operator-facing rollout
posture on the licensed settings surface: relay paywall and pairing copy must
describe supported Pulse Mobile pairing as a normal-use Relay capability, and
must not fall back to staged-beta or coming-soon messaging once the owned
pairing/runtime path is live.
The customer-account surface is now also an explicit cloud-paid ownership
boundary. Pulse now has a coherent authenticated Pulse Account shell in
internal/cloudcp/portal/, account/workspace mutation APIs in
internal/cloudcp/account/tenant_handlers.go, and transitional self-hosted
commercial utility pages in pulse-pro/landing-page/. The canonical product
shape is docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md: one
authenticated Pulse account shell that unifies Cloud tenants, self-hosted
licenses, billing, recovery, and MSP admin surfaces without creating a
standalone Relay portal. That shell now also owns two product rules
explicitly: signed-in hosted arrivals land on Workspaces, not on a separate
Overview or Summary destination, and the top of Workspaces may render
only one quiet inline facts line plus one next-action row before the workspace
list instead of a second summary deck, metric grid, or duplicated overview
panel. The top-level task row must stay honest to account shape by removing
irrelevant hosted-only tasks instead of pretending they are live. Any shared
fallback that still lands on a non-live task must render an explicit
unavailable state instead of blank space. The same shell also owns
action-first task surfaces for Access, Billing, and Support: access
mutations must be permission-honest and roster-led, billing must reduce to one
obvious job at a time with hosted billing first when relevant, support must
stay a failed-path handoff rather than a peer workflow, and phone-width
layouts must collapse the desktop shell into a compact task strip so the
active job remains primary and visibly in-frame when the strip scrolls. The
same narrow-screen shell must also compress account identity into one compact
context strip instead of repeating a desktop-sized intro block or second
summary box ahead of every task, and lower
workspace job surfaces such as lifecycle review or create-workspace forms must
be revealed when the user opens them. Workspaces must also default to the
workspace list plus the real task entry points rather than an idle lifecycle
essay; the lifecycle rail should appear only when a lifecycle or
create-workspace job is active. Access follows the same rule: the hosted
roster must be the default state, while invite, role-change, and remove
controls appear only when that exact access job is active, and a hosted
view-only roster must stay a review surface instead of a row-by-row action
table with fake disabled action state. Managed idle Access follows that same
review-first rule: the roster should stay two-column until a remove job is
active, with the third action column reserved for real remove access work.
That same task-first Access contract
must also start from bootstrap-owned roster truth: the first hosted roster
render must come from the portal bootstrap payload itself, with later member
API reads reserved for refresh and mutation follow-through instead of
placeholder-first rendering. That same hosted access boundary also owns
invitation consent semantics: internal/cloudcp/account/handlers.go plus
internal/cloudcp/registry/models.go and internal/cloudcp/registry/registry.go
may persist pending invitation subjects for unknown emails, but they must not
auto-create users or memberships from invite input alone. Pending access rows
must remain explicit pending invitation subjects in the bootstrap roster and
member API until the invited email authenticates through the portal-owned
magic-link/session path in internal/cloudcp/auth/handlers.go and
internal/cloudcp/auth/session.go, and role or removal mutations on those
rows must target the invitation record rather than guessing a future user
identity. Billing follows
the same task-first rule: hosted billing leads when present, the self-hosted
billing, license, refund, and privacy paths reduce to explicit job pickers,
and the active self-hosted billing panel must stay hidden until that exact job
is opened and then be revealed in-frame on narrow layouts. When no hosted
account exists, the billing shell must skip any empty hosted-billing block and
lead directly with the self-hosted job picker. Support follows
the same account-shape rule: self-hosted-only accounts reduce to the billing
escalation path and billing-specific handoff packet only, and hosted
workspace/access escalation routes must not render without hosted accounts.
That same owned shell also owns the signed-in visual posture: the portal
should read like a serious settings/account tool rather than a dashboard.
Navigation chrome, account framing, and inline workspace summary treatment
must stay visually quieter than the active task surface, with flatter light
treatment and list-first task presentation instead of dark ornamental rails,
sidebars, or nested explanatory cards. The signed-in shell should orient the
user with one quiet account-context header and one flat top task row; the
Workspaces surface may add one factual inline summary line and one next-step
row inside the section itself, but it must not bring back a second summary
deck competing with the task.
That same owned shell must also open on the first live job instead of a
summary layer: hosted accounts should land in Workspaces, and self-hosted-
only accounts should land in Billing. The signed-in shell must not expose a
separate Overview or Summary tab ahead of the real task surfaces.
That same owned shell also owns the signed-out visual posture: the unauthenticated
/portal page must read like the same product boundary, not a leftover
marketing block plus a generic login card. The auth surface should present one
obvious sign-in action, precise account-scope rows, and the same restrained
flat system as the signed-in account shell.
The same shell/runtime boundary must also stay honest to hosted-account
permissions: when the current role is view-only, Workspaces, Access, and
hosted Billing copy must not advertise create, roster-mutation, or hosted
billing actions that the runtime will not allow. Those surfaces must say that
an owner or admin is required instead of implying blocked jobs are live.
Support follows that same permission rule: a hosted view-only user may be
sent back to Workspaces, Access, or Billing only as review and
owner/admin handoff paths, not as though the user can perform hosted
lifecycle, access-mutation, or hosted-billing changes directly before
escalation.
Inline workspace counts and shell copy follow the same account-shape and
permission rule: hosted-only accounts must not mention self-hosted billing
utilities by default, and hosted view-only roles must say when hosted billing
still needs owner/admin authority.
That same portal runtime contract now also owns explicit self-hosted upgrade
arrivals from the product: when Pulse hands a self-hosted commercial upgrade
intent into /portal, the billing shell may expose an Upgrade job even when
the signed-in account has no existing self-hosted commercial history, but that
arrival must stay narrower than the full self-hosted utility set. Hosted-only
accounts may not suddenly render retrieve/refund/privacy tools by default just
because they arrived from an upgrade CTA; only the specific portal-owned
upgrade path may appear until the account actually has relevant self-hosted
history.
The same rule applies to the compact account-context strip: it must describe
the current user's effective hosted tasks, not restate full access-control and
billing capability when those actions are blocked behind owner/admin roles.
That same owned shell must also keep role labels on product vocabulary:
customer-facing copy may say Owner, Admin, Tech, or Read-only, but it
must not leak internal identifiers such as read_only or legacy aliases such
as member.
That same owned signed-in shell must also keep the first available action
permission-honest for hosted view-only users: when no workspace is ready, the
primary route must stay on reviewable Workspaces or Access surfaces before
any blocked hosted billing or owner/admin-only mutation path.
That same owned task surface must also keep failure copy on the user job
instead of leaking raw transport wording: Access, Workspaces, and
Billing failures must render the task-specific action that could not
complete, not generic copy such as Network error..
That same owned inline workspace counts and workspace-state copy must also
keep Ready honest when no hosted workspace exists yet: hosted accounts with
zero workspaces may not tell the user to review current workspace state, and
must instead say that nothing is ready yet until the first hosted workspace
exists. The same counts and copy must keep suspended-only states honest, and
must stay fact-first rather than inventing urgency or health verdicts such as
Nothing urgent or Healthy now.
That same portal shell/runtime boundary must also keep task and status copy
literal across the account surface: customer-facing wording may not lean on
commentary such as obvious, actual work, trustworthy, or settled when
the runtime already knows the concrete state, action, or failure being shown.
That includes shell badges, section labels, context chips, route labels, and
error headings: they must use the exact action or state (Manage access,
Hosted billing attached, Email support, Failed to load roster) instead
of shorthand such as Manage, Hosted, or generic alert labels. Support
copy follows the same contract: escalation guidance must stay short and typed
to the exact task path, account/email, and failed step instead of drifting
into longer procedural prose.
That same canonical shell/runtime boundary now also owns the bootstrap truth
for when self-hosted commercial history is relevant. Hosted-only accounts must
not render self-hosted license, refund, privacy, or support-escalation copy
unless the portal bootstrap explicitly marks self-hosted commercial history as
relevant to the signed-in account.
Until that
candidate lane lands, new
commercial account work must extend the governed Pulse account shape rather
than spawning additional one-off recovery or billing pages.
The same owned shell/runtime boundary also keeps signed-in bootstrap free of
feature-local telemetry collectors. frontend-modern/src/useAppRuntimeState.ts
may start shared app-shell services, auth refresh, and websocket recovery, but
it must not boot a storage-only disk history collector now that storage
drawers read the canonical backend metrics-history contract through shared
chart primitives.
That same hosted browser shell boundary now also treats
frontend-modern/src/App.tsx as provider placement only. Shared runtime
consumers must import websocket and dark-mode hooks from
frontend-modern/src/contexts/appRuntime.ts instead of importing @/App, so
lazy hosted and settings chunks cannot create a reverse dependency into the
app shell and blank the mounted browser surface before auth/bootstrap
completes.
That same cloud-paid/browser boundary now also governs public demo posture.
DEMO_MODE may run against a real internal entitlement, but public demo
surfaces must not reveal self-hosted license metadata, hosted billing state,
monitored-system ledgers, upgrade nudges, or activation controls just because
the underlying runtime is commercially enabled.
internal/api/router_routes_auth_security.go,
internal/api/security_status_capabilities.go,
frontend-modern/src/useAppRuntimeState.ts, and the shared session-capability
stores must treat /api/security/status.sessionCapabilities.demoMode as the
canonical browser bootstrap signal, and shared billing or upgrade surfaces
must hide or suppress themselves from that contract rather than teaching mock
mode, response-header inference, or frontend-only feature flags to bypass the
real licensing model. That includes settings billing tabs, public-demo banner
and monitored-system/trial nudges, app-wide relay paywalls, Patrol upgrade
CTAs, and history-lock upsells. Demo readiness therefore means presentation
isolation, not a license exemption.
That same bootstrap owner must also avoid protected pre-auth noise. When auth
is configured but the browser is still on a public login entrypoint (/ or
/login) with no local auth hint yet, frontend-modern/src/useAppRuntimeState.ts
must stop at the shared login-needed state and skip the /api/state probe, so
public demos and other browser-first login surfaces do not emit avoidable
401 traffic before a session exists. Once the local login flow has written
its bootstrap hint or the operator is landing on a protected route, the
canonical state probe still owns authenticated runtime detection.
That same presentation-policy contract now owns the default self-hosted v6
commercial posture. Outside hosted mode, ordinary self-hosted installs must
fail closed on in-app upgrade prompts: Relay/Pro plan comparison, Pro trial
CTAs, paid-only settings navigation, and history/feature upsells may render
only for explicit handoff/direct routes, activation or recovery state, hosted
mode, or an already entitled install. Trial checkout plumbing may remain only
as legacy/support or externally initiated compatibility, but it is not a normal
GA self-hosted app journey.
Shared self-hosted plan presentation helpers must carry that same policy:
Community copy may mention provider/local-model Patrol, but must not present
hosted-model credits, account-backed AI access, or trial acquisition as a
default self-hosted benefit or a reason to put a paid prompt in front of
ordinary users.
That public-demo commercial boundary also owns monitored-system preview
unavailability wording. Browser presentation may keep the unavailable reason
nullable until the formatting edge, but it must normalize the message through
the shared monitored-system presentation helper instead of branching on
demo/billing state inside settings panels or inventing a second mock-only
license explanation path.