Skip to Content
DocsCloud OSMSP Portal

MSP Customer Portal

Phase Г.1 of the GA roadmap: a white-label customer portal where the END customer of an MSP logs in and sees only THEIR scoped view. Operator-facing CP stays untouched; the portal is a parallel surface mounted at /portal/<tenant_slug>/.

Per-tenant branding

client_branding table (already shipped, extended in Г.1):

ColumnPurpose
logo_urlHeader logo
primary_colorCSS --brand-primary token
secondary_coloraccent
custom_domain”billing.acmecorp.com” — Caddy + ACME wires this up; CP resolves the right tenant via Host header
portal_enabledfeature flag (Team+ only)
support_emailshown on the contact card

Operators edit branding via CP → MSP → Branding; portal owners can override their own branding via /portal/<slug>/branding.

Customer roles

portal_users table — three roles within each tenant:

  • owner — everything, plus invite/remove portal users + edit branding.
  • admin — read all + restart/upgrade nodes within the tenant.
  • viewer — read-only.

Auth is JWT cookie (same machinery as the operator-facing CP, but with a separate audience claim so the two are isolated).

Scoped views

Seven views, all auto-scoped to the resolved client_id:

ViewSourceScope
Dashboardaggregated node metricstenant’s nodes only
Nodes list + detailinternal/fleet/nodes.gofiltered by client_id
Billinginternal/msp/billing.gotenant’s invoices + plan
Tickets (read-only)internal/ticketstenant-scoped
Audit log (last 30d)CP audittenant-scoped (admin+)
Profile / portal usersportal_usersclient_id-scoped
Branding (owner only)client_brandingtenant’s row

Anything fleet-wide (cross-tenant nodes, MSP partner settings, license sales) is never exposed via the portal.

REST surface

POST /portal/{slug}/auth/login { email, password } → { jwt } POST /portal/{slug}/auth/logout — clears cookie GET /portal/{slug}/state → { user, branding, nav } GET /portal/{slug}/dashboard → aggregated metrics GET /portal/{slug}/nodes → list GET /portal/{slug}/nodes/{id} → detail POST /portal/{slug}/nodes/{id}/restart → 202 (admin+) GET /portal/{slug}/billing → plan + last 12 invoices GET /portal/{slug}/tickets → list (tenant-scoped) GET /portal/{slug}/audit?since=… → recent events (admin+) GET /portal/{slug}/users → list portal_users (owner) POST /portal/{slug}/users → invite (owner) PUT /portal/{slug}/users/{id} → role update (owner) DELETE /portal/{slug}/users/{id} → (owner) GET /portal/{slug}/branding → current PUT /portal/{slug}/branding → update (owner)

Tenant resolver matches Host header (custom domain) before falling back to URL slug.

Plans

TierPortal availability
Free / Pron/a — feature is MSP-only
Team✅ (1 portal, ≤5 portal users)
Enterprise✅ (unlimited portals, unlimited portal users)

License gates: msp_portal (Team+), msp_portal_users_cap (Team=5 / Enterprise=∞).

Bundle

The portal SPA lives at quazzar-cloud-panel/web-portal/ (Vite + React 19 + TanStack Query v5). Built bundle embeds at quazzar-cloud-panel/internal/static/portal-dist/ and is served via SPA fallback at /portal/{slug}/*.