' + escapeHTML(account.can_manage ? "No hosted workspaces yet. Create one to get started." : "No hosted workspaces are attached yet. An owner or admin must create the first one.") + "
This preview page stands in for the hosted client workspace. In production the signed handoff creates a session inside the client workspace and opens ' + escapeHTML(targetLabel) + '.
' +
+ '
' + escapeHTML(targetHeading) + '
' +
+ '
You are inside the ' + escapeHTML(title) + ' client workspace. In production the signed handoff creates a tenant session and opens ' + escapeHTML(targetLabel) + '.
Use one commercial email address for hosted workspaces, account access, billing, licenses, refunds, and privacy requests.
' +
'
' +
- renderAuthScopeRow('Workspaces', 'Open hosted workspaces and review lifecycle state.') +
+ renderAuthScopeRow('Workspaces', 'Open hosted workspaces and review workspace state.') +
renderAuthScopeRow('Access', 'Review account access and manage roles when permitted.') +
renderAuthScopeRow('Billing', 'Open hosted billing or self-hosted commercial tools when they apply.') +
'
' +
diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css
index e46bd401c..e1917bfee 100644
--- a/internal/cloudcp/portal/frontend/src/styles.css
+++ b/internal/cloudcp/portal/frontend/src/styles.css
@@ -321,7 +321,7 @@ header .logout-btn:hover,
.workspace-list-head {
display: grid;
- grid-template-columns: minmax(0, 2fr) 80px 80px 160px;
+ grid-template-columns: minmax(12rem, 1fr) 112px 88px minmax(300px, auto);
gap: 8px;
padding: 6px 16px;
background: var(--bg-subtle);
@@ -338,7 +338,7 @@ header .logout-btn:hover,
.workspace-row {
display: grid;
- grid-template-columns: minmax(0, 2fr) 80px 80px 160px;
+ grid-template-columns: minmax(12rem, 1fr) 112px 88px minmax(300px, auto);
gap: 8px;
align-items: center;
padding: 10px 16px;
@@ -405,11 +405,13 @@ header .logout-btn:hover,
.workspace-actions {
display: flex;
align-items: center;
+ flex-wrap: wrap;
gap: 6px;
justify-content: flex-end;
}
-.workspace-actions form { margin: 0; }
+.workspace-actions form { flex: 0 0 auto; margin: 0; }
+.workspace-actions button { white-space: nowrap; }
/* ── Badges ───────────────────────────────────────────────────── */
.badge {
@@ -431,6 +433,11 @@ header .logout-btn:hover,
.badge-checking, .badge-provisioning { border-color: #c6d9f0; background: var(--accent-subtle); color: var(--accent); }
.badge-suspended { border-color: #e3c97a; background: var(--warn-subtle); color: var(--warn); }
.badge-deleting { border-color: #f5b3b3; background: var(--danger-subtle); color: var(--danger); }
+.badge-setup-ready { border-color: #aedcb5; background: var(--success-subtle); color: var(--success); }
+.badge-setup-setup_path { border-color: #c6d9f0; background: var(--accent-subtle); color: var(--accent); }
+.badge-setup-install_agents,
+.badge-setup-configure_outputs { border-color: #e3c97a; background: var(--warn-subtle); color: var(--warn); }
+.badge-setup-review { border-color: #f5b3b3; background: var(--danger-subtle); color: var(--danger); }
/* ── Workspace management panel ───────────────────────────────── */
.workspace-operations-shell {
@@ -519,6 +526,78 @@ header .logout-btn:hover,
color: var(--ink-secondary);
}
+.workspace-setup-checklist {
+ display: grid;
+ gap: 8px;
+}
+
+.workspace-setup-step {
+ display: grid;
+ grid-template-columns: 78px minmax(0, 1fr);
+ gap: 10px;
+ align-items: start;
+ padding: 10px 12px;
+ border: 1px solid var(--border-muted);
+ border-radius: var(--radius);
+ background: var(--bg-subtle);
+}
+
+.workspace-setup-step strong,
+.workspace-setup-step span {
+ display: block;
+}
+
+.workspace-setup-step strong {
+ color: var(--ink);
+ font-size: 13px;
+}
+
+.workspace-setup-step div > span {
+ margin-top: 2px;
+ color: var(--ink-secondary);
+ font-size: 12px;
+}
+
+.workspace-setup-status {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 22px;
+ padding: 2px 7px;
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ background: var(--bg);
+ color: var(--ink-secondary);
+ font-size: 11px;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.workspace-setup-status-done,
+.workspace-setup-status-available {
+ border-color: #aedcb5;
+ background: var(--success-subtle);
+ color: var(--success);
+}
+
+.workspace-setup-status-next {
+ border-color: #e3c97a;
+ background: var(--warn-subtle);
+ color: var(--warn);
+}
+
+.workspace-setup-status-blocked {
+ border-color: #f5b3b3;
+ background: var(--danger-subtle);
+ color: var(--danger);
+}
+
+.workspace-setup-status-pending {
+ border-color: #c6d9f0;
+ background: var(--accent-subtle);
+ color: var(--accent);
+}
+
.workspace-management-next-steps {
display: flex;
flex-direction: column;
@@ -1449,6 +1528,7 @@ header .logout-btn:hover,
.workspace-list-head { display: none; }
.workspace-row { grid-template-columns: 1fr; gap: 8px; }
+ .workspace-list-head { display: none; }
.workspace-actions { justify-content: flex-start; width: 100%; }
.access-roster-head,
@@ -1464,6 +1544,7 @@ header .logout-btn:hover,
.portal-support-simple-row { grid-template-columns: 1fr; gap: 4px; }
.workspace-management-facts { grid-template-columns: 1fr; }
+ .workspace-setup-step { grid-template-columns: 1fr; }
.workspace-next-step { grid-template-columns: 1fr; }
.workspace-next-step form { justify-self: flex-start; }
}
diff --git a/internal/cloudcp/portal/frontend/src/types.ts b/internal/cloudcp/portal/frontend/src/types.ts
index 6864efe71..7f9582a53 100644
--- a/internal/cloudcp/portal/frontend/src/types.ts
+++ b/internal/cloudcp/portal/frontend/src/types.ts
@@ -4,6 +4,10 @@ export interface PortalWorkspaceSummary {
state: string;
healthy: boolean;
health_status: 'healthy' | 'checking' | 'unhealthy';
+ setup_status?: 'ready' | 'setup_path' | 'install_agents' | 'configure_outputs' | 'review';
+ agent_count?: number;
+ alert_route_count?: number;
+ report_schedule_count?: number;
last_health_check?: string;
created_at?: string;
}
diff --git a/internal/cloudcp/portal/frontend/src/workspace_presentation.test.ts b/internal/cloudcp/portal/frontend/src/workspace_presentation.test.ts
index 5030c63fe..dd3a4ad36 100644
--- a/internal/cloudcp/portal/frontend/src/workspace_presentation.test.ts
+++ b/internal/cloudcp/portal/frontend/src/workspace_presentation.test.ts
@@ -1,6 +1,15 @@
import { describe, expect, it } from 'vitest';
-import { workspaceGuidanceCopy, workspaceHealthLabel, workspaceHealthState, workspaceRowNote, workspaceStatusCopy } from './workspace_presentation';
+import {
+ workspaceGuidanceCopy,
+ workspaceHealthLabel,
+ workspaceHealthState,
+ workspaceRowNote,
+ workspaceSetupLabel,
+ workspaceSetupNextStep,
+ workspaceSetupState,
+ workspaceStatusCopy,
+} from './workspace_presentation';
import type { PortalWorkspaceSummary } from './types';
function createWorkspace(overrides: Partial = {}): PortalWorkspaceSummary {
@@ -16,7 +25,7 @@ function createWorkspace(overrides: Partial = {}): Porta
describe('workspace presentation', function() {
it('derives consistent literal copy for healthy, pending, unhealthy, and suspended workspaces', function() {
- var healthy = createWorkspace();
+ var healthy = createWorkspace({ setup_status: 'ready' });
var checking = createWorkspace({ healthy: false, health_status: 'checking' });
var unhealthy = createWorkspace({ healthy: false, health_status: 'unhealthy' });
var suspended = createWorkspace({ state: 'suspended', health_status: 'healthy' });
@@ -40,10 +49,22 @@ describe('workspace presentation', function() {
expect(workspaceRowNote(suspended)).toBe('Suspended');
});
- it('uses literal lifecycle guidance instead of commentary', function() {
- expect(workspaceGuidanceCopy(createWorkspace())).toBe('This workspace is active. Open it to work inside this client boundary, or install agents to reach the workspace-bound install commands.');
+ it('uses literal setup guidance instead of commentary', function() {
+ expect(workspaceGuidanceCopy(createWorkspace({ setup_status: 'ready' }))).toBe('Open the workspace when you need to work inside this client boundary.');
+ expect(workspaceGuidanceCopy(createWorkspace())).toBe('Open the workspace or install agents from the workspace-bound setup path.');
expect(workspaceGuidanceCopy(createWorkspace({ healthy: false, health_status: 'checking' }))).toBe('This workspace is active. The latest health check is still pending, but the workspace can still own agent install commands.');
expect(workspaceGuidanceCopy(createWorkspace({ healthy: false, health_status: 'unhealthy' }))).toBe('The latest health check is unhealthy. Review the current state before suspending or deleting this workspace.');
- expect(workspaceGuidanceCopy(createWorkspace({ state: 'suspended', health_status: 'healthy' }))).toBe('This workspace is suspended. The remaining lifecycle action here is deletion.');
+ expect(workspaceGuidanceCopy(createWorkspace({ state: 'suspended', health_status: 'healthy' }))).toBe('This workspace is suspended. The remaining destructive action here is deletion.');
+ });
+
+ it('derives setup state from explicit status and optional setup counts', function() {
+ expect(workspaceSetupState(createWorkspace())).toBe('setup_path');
+ expect(workspaceSetupLabel(createWorkspace())).toBe('Setup path');
+ expect(workspaceRowNote(createWorkspace())).toBe('Setup path ready');
+ expect(workspaceSetupNextStep(createWorkspace({ setup_status: 'install_agents' }))).toBe('Install the first agent from this workspace so client data lands in the right boundary.');
+ expect(workspaceSetupState(createWorkspace({ agent_count: 0 }))).toBe('install_agents');
+ expect(workspaceSetupState(createWorkspace({ agent_count: 1, alert_route_count: 0, report_schedule_count: 0 }))).toBe('configure_outputs');
+ expect(workspaceSetupState(createWorkspace({ agent_count: 1, alert_route_count: 1, report_schedule_count: 1 }))).toBe('ready');
+ expect(workspaceSetupState(createWorkspace({ state: 'failed', healthy: false, health_status: 'unhealthy' }))).toBe('review');
});
});
diff --git a/internal/cloudcp/portal/frontend/src/workspace_presentation.ts b/internal/cloudcp/portal/frontend/src/workspace_presentation.ts
index e0bc189df..a1f6ffe09 100644
--- a/internal/cloudcp/portal/frontend/src/workspace_presentation.ts
+++ b/internal/cloudcp/portal/frontend/src/workspace_presentation.ts
@@ -1,5 +1,7 @@
import type { PortalWorkspaceSummary } from './types';
+export type WorkspaceSetupState = 'ready' | 'setup_path' | 'install_agents' | 'configure_outputs' | 'review';
+
export function workspaceHealthState(workspace: PortalWorkspaceSummary): 'healthy' | 'checking' | 'unhealthy' {
if (workspace.health_status === 'healthy' || workspace.health_status === 'checking' || workspace.health_status === 'unhealthy') {
return workspace.health_status;
@@ -31,16 +33,100 @@ export function workspaceRowNote(workspace: PortalWorkspaceSummary): string {
var state = String(workspace.state || '');
if (state === 'suspended') return 'Suspended';
if (state === 'failed') return 'Failed';
- if (status === 'healthy') return 'Ready';
+ if (status === 'checking') return 'Health check pending';
if (status === 'unhealthy') return 'Unhealthy';
- return 'Health check pending';
+ var setup = workspaceSetupState(workspace);
+ if (setup === 'ready') return 'Ready';
+ if (setup === 'install_agents') return 'Install first agent';
+ if (setup === 'configure_outputs') return 'Configure alerts and reports';
+ if (setup === 'setup_path') return 'Setup path ready';
+ if (setup === 'review') return 'Review';
+ return 'Ready';
+}
+
+function hasNumber(value: unknown): boolean {
+ return typeof value === 'number' && Number.isFinite(value);
+}
+
+function positiveCount(value: unknown): boolean {
+ return hasNumber(value) && Number(value) > 0;
+}
+
+function explicitSetupStatus(workspace: PortalWorkspaceSummary): WorkspaceSetupState | '' {
+ var value = String(workspace.setup_status || '');
+ if (
+ value === 'ready' ||
+ value === 'setup_path' ||
+ value === 'install_agents' ||
+ value === 'configure_outputs' ||
+ value === 'review'
+ ) {
+ return value;
+ }
+ return '';
+}
+
+export function workspaceSetupState(workspace: PortalWorkspaceSummary): WorkspaceSetupState {
+ var explicit = explicitSetupStatus(workspace);
+ if (explicit) return explicit;
+
+ var state = String(workspace.state || '');
+ var health = workspaceHealthState(workspace);
+ if (state === 'suspended' || state === 'failed' || health === 'unhealthy') return 'review';
+ if (state !== 'active' || health === 'checking') return 'setup_path';
+
+ var knowsAgentCount = hasNumber(workspace.agent_count);
+ var knowsAlertCount = hasNumber(workspace.alert_route_count);
+ var knowsReportCount = hasNumber(workspace.report_schedule_count);
+ var hasAgents = positiveCount(workspace.agent_count);
+ var hasAlerts = positiveCount(workspace.alert_route_count);
+ var hasReports = positiveCount(workspace.report_schedule_count);
+
+ if (knowsAgentCount && !hasAgents) return 'install_agents';
+ if (hasAgents && ((knowsAlertCount && !hasAlerts) || (knowsReportCount && !hasReports))) {
+ return 'configure_outputs';
+ }
+ if (hasAgents && (!knowsAlertCount || hasAlerts) && (!knowsReportCount || hasReports)) {
+ return 'ready';
+ }
+ return 'setup_path';
+}
+
+export function workspaceSetupLabel(workspace: PortalWorkspaceSummary): string {
+ switch (workspaceSetupState(workspace)) {
+ case 'ready':
+ return 'Ready';
+ case 'install_agents':
+ return 'Install agent';
+ case 'configure_outputs':
+ return 'Configure outputs';
+ case 'review':
+ return 'Review';
+ default:
+ return 'Setup path';
+ }
+}
+
+export function workspaceSetupNextStep(workspace: PortalWorkspaceSummary): string {
+ switch (workspaceSetupState(workspace)) {
+ case 'ready':
+ return 'Open the workspace when you need to work inside this client boundary.';
+ case 'install_agents':
+ return 'Install the first agent from this workspace so client data lands in the right boundary.';
+ case 'configure_outputs':
+ return 'Configure alert routing and reports before treating the client workspace as ready.';
+ case 'review':
+ return 'Review the workspace state before continuing setup.';
+ default:
+ return 'Open the workspace or install agents from the workspace-bound setup path.';
+ }
}
export function workspaceGuidanceCopy(workspace: PortalWorkspaceSummary): string {
var status = workspaceHealthState(workspace);
var state = String(workspace.state || '');
if (state === 'active' && status === 'healthy') {
- return 'This workspace is active. Open it to work inside this client boundary, or install agents to reach the workspace-bound install commands.';
+ return workspaceSetupNextStep(workspace);
}
if (state === 'active' && status === 'checking') {
return 'This workspace is active. The latest health check is still pending, but the workspace can still own agent install commands.';
@@ -49,7 +135,7 @@ export function workspaceGuidanceCopy(workspace: PortalWorkspaceSummary): string
return 'The latest health check is unhealthy. Review the current state before suspending or deleting this workspace.';
}
if (state === 'suspended') {
- return 'This workspace is suspended. The remaining lifecycle action here is deletion.';
+ return 'This workspace is suspended. The remaining destructive action here is deletion.';
}
- return 'Review the current lifecycle state before taking action on this workspace.';
+ return 'Review the current workspace state before taking action on this workspace.';
}
diff --git a/internal/cloudcp/portal/handlers.go b/internal/cloudcp/portal/handlers.go
index 950becc9c..ebba81679 100644
--- a/internal/cloudcp/portal/handlers.go
+++ b/internal/cloudcp/portal/handlers.go
@@ -25,6 +25,7 @@ type workspaceSummaryItem struct {
DisplayName string `json:"display_name"`
State registry.TenantState `json:"state"`
HealthCheckOK bool `json:"health_check_ok"`
+ SetupStatus string `json:"setup_status,omitempty"`
LastHealthCheck *time.Time `json:"last_health_check"`
CreatedAt time.Time `json:"created_at"`
}
@@ -124,6 +125,7 @@ func HandlePortalDashboard(reg *registry.TenantRegistry) http.HandlerFunc {
DisplayName: t.DisplayName,
State: t.State,
HealthCheckOK: t.HealthCheckOK,
+ SetupStatus: workspaceSetupStatus(t.State, t.HealthCheckOK, t.LastHealthCheck),
LastHealthCheck: t.LastHealthCheck,
CreatedAt: t.CreatedAt,
})
diff --git a/internal/cloudcp/portal/handlers_test.go b/internal/cloudcp/portal/handlers_test.go
index 0c8055a72..a10157f00 100644
--- a/internal/cloudcp/portal/handlers_test.go
+++ b/internal/cloudcp/portal/handlers_test.go
@@ -119,6 +119,7 @@ type dashboardResp struct {
DisplayName string `json:"display_name"`
State registry.TenantState `json:"state"`
HealthCheckOK bool `json:"health_check_ok"`
+ SetupStatus string `json:"setup_status"`
LastHealthCheck *time.Time `json:"last_health_check"`
CreatedAt time.Time `json:"created_at"`
} `json:"workspaces"`
@@ -240,6 +241,7 @@ func TestPortalDashboard(t *testing.T) {
DisplayName: ws.DisplayName,
State: ws.State,
HealthCheckOK: ws.HealthCheckOK,
+ SetupStatus: ws.SetupStatus,
LastHealthCheck: ws.LastHealthCheck,
CreatedAt: ws.CreatedAt,
}
@@ -258,6 +260,9 @@ func TestPortalDashboard(t *testing.T) {
if !active.HealthCheckOK {
t.Fatalf("active.health_check_ok = false, want true")
}
+ if active.SetupStatus != "setup_path" {
+ t.Fatalf("active.setup_status = %q, want setup_path", active.SetupStatus)
+ }
if active.LastHealthCheck == nil || !active.LastHealthCheck.Equal(lastCheck) {
t.Fatalf("active.last_health_check = %v, want %v", active.LastHealthCheck, lastCheck)
}
@@ -272,6 +277,9 @@ func TestPortalDashboard(t *testing.T) {
if susp.State != registry.TenantStateSuspended {
t.Fatalf("suspended.state = %q, want %q", susp.State, registry.TenantStateSuspended)
}
+ if susp.SetupStatus != "review" {
+ t.Fatalf("suspended.setup_status = %q, want review", susp.SetupStatus)
+ }
if resp.Summary.Total != 2 {
t.Fatalf("summary.total = %d, want %d", resp.Summary.Total, 2)
@@ -295,6 +303,7 @@ type dashboardRespWorkspace struct {
DisplayName string
State registry.TenantState
HealthCheckOK bool
+ SetupStatus string
LastHealthCheck *time.Time
CreatedAt time.Time
}
@@ -915,6 +924,7 @@ func TestBuildPortalBootstrapJSON_Contract(t *testing.T) {
State: "active",
Healthy: true,
HealthStatus: "healthy",
+ SetupStatus: "setup_path",
LastHealthCheck: &lastHealthCheck,
},
},
@@ -1012,6 +1022,9 @@ func TestBuildPortalBootstrapJSON_Contract(t *testing.T) {
if got := workspace["health_status"]; got != "healthy" {
t.Fatalf("workspace health_status = %#v, want healthy", got)
}
+ if got := workspace["setup_status"]; got != "setup_path" {
+ t.Fatalf("workspace setup_status = %#v, want setup_path", got)
+ }
if got := workspace["last_health_check"]; got != "2026-03-27T09:00:00Z" {
t.Fatalf("workspace last_health_check = %#v, want 2026-03-27T09:00:00Z", got)
}
@@ -1092,6 +1105,9 @@ func TestHandlePortalBootstrap_Success(t *testing.T) {
if got := workspace["created_at"]; got != "2026-03-25T10:00:00Z" {
t.Fatalf("workspace created_at = %#v", got)
}
+ if got := workspace["setup_status"]; got != "setup_path" {
+ t.Fatalf("workspace setup_status = %#v, want setup_path", got)
+ }
}
func TestHandlePortalBootstrap_RequiresAuth(t *testing.T) {
diff --git a/internal/cloudcp/portal/page.go b/internal/cloudcp/portal/page.go
index 6dcd33790..5fe31032a 100644
--- a/internal/cloudcp/portal/page.go
+++ b/internal/cloudcp/portal/page.go
@@ -19,6 +19,7 @@ type portalPageWorkspace struct {
State string
Healthy bool
HealthStatus string
+ SetupStatus string
LastHealthCheck *time.Time
CreatedAt time.Time
}
@@ -175,6 +176,7 @@ func loadPortalAccountsForUser(reg *registry.TenantRegistry, userID string) ([]p
State: string(t.State),
Healthy: t.HealthCheckOK,
HealthStatus: workspaceHealthStatus(t.HealthCheckOK, t.LastHealthCheck),
+ SetupStatus: workspaceSetupStatus(t.State, t.HealthCheckOK, t.LastHealthCheck),
LastHealthCheck: t.LastHealthCheck,
CreatedAt: t.CreatedAt,
})
@@ -249,6 +251,20 @@ func workspaceHealthStatus(healthy bool, lastHealthCheck *time.Time) string {
return "unhealthy"
}
+func workspaceSetupStatus(state registry.TenantState, healthy bool, lastHealthCheck *time.Time) string {
+ switch state {
+ case registry.TenantStateActive:
+ if !healthy && lastHealthCheck != nil {
+ return "review"
+ }
+ return "setup_path"
+ case registry.TenantStateProvisioning:
+ return "setup_path"
+ default:
+ return "review"
+ }
+}
+
func renderPortalPage(w http.ResponseWriter, nonce string, faviconHref string, bootstrapData BootstrapData) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
diff --git a/internal/cloudcp/public_msp_signup_handlers.go b/internal/cloudcp/public_msp_signup_handlers.go
index d8c4dfb72..c6923aaa5 100644
--- a/internal/cloudcp/public_msp_signup_handlers.go
+++ b/internal/cloudcp/public_msp_signup_handlers.go
@@ -139,7 +139,7 @@ var publicMSPSignupPageTemplate = template.Must(template.New("public-msp-signup-
Start Pulse MSP Starter
-
Run Pulse for multiple clients from one hosted operator account. Each client gets an isolated workspace; you manage them all from the MSP portal. Starter is the self-serve checkout path for small providers. Growth and Scale are request-based so onboarding, reporting, and support expectations can be sized with you.
+
Run Pulse for multiple clients from one hosted operator account. Each client gets an isolated workspace; you manage them all from Pulse Account. Starter is the self-serve checkout path for small providers. Growth and Scale have published prices, but access is request-assisted so onboarding, reporting, and support expectations can be sized with you.
{{if .ErrorMessage}}
{{.ErrorMessage}}
{{end}}
{{if .Cancelled}}
Checkout was cancelled. You can start again below.
{{end}}
@@ -148,7 +148,8 @@ var publicMSPSignupPageTemplate = template.Must(template.New("public-msp-signup-
Starter — up to 5 client workspaces, $149/mo
-
Growth / Scale — request access for 15+ client workspaces, custom onboarding, or assisted rollout.
+
Growth — up to 15 client workspaces, $249/mo, request-assisted access
+
Scale — up to 40 client workspaces, $399/mo, request-assisted access
@@ -160,7 +161,7 @@ var publicMSPSignupPageTemplate = template.Must(template.New("public-msp-signup-
-
After checkout, we will email a Pulse Account sign-in link so you can open your MSP portal. For Growth or Scale, email support@pulserelay.pro and include your expected client workspace count.
+
After checkout, we will email a Pulse Account sign-in link so you can create client workspaces and continue setup. For Growth or Scale, email support@pulserelay.pro and include the tier you want and your expected client workspace count.
Stripe securely starts your MSP Starter subscription.
Pulse provisions your MSP operator account after checkout completes.
@@ -195,7 +196,7 @@ var publicMSPSignupCompleteTemplate = template.Must(template.New("public-msp-sig
Checkout complete
Your Pulse MSP Starter checkout completed. Pulse is provisioning your MSP operator account.
-
Watch your inbox for a Pulse Account sign-in link. That link lands in Pulse Account, where you can open the MSP portal, add client workspaces, and continue setup.
+
Watch your inbox for a Pulse Account sign-in link. That link lands in Pulse Account, where you can add client workspaces and continue setup.