Audited the customer-facing docs against the code: about 5,200 concrete claims (env vars, endpoints, flags, defaults, ports, UI labels), plus every environment variable the code reads. Each discrepancy was re-verified against the source before editing, and every edit was reviewed again. What users would have hit following the old text: - UI paths and labels that no longer exist (Settings > Relay, Platform connections, Access Control, SSO Providers, Create Organization) and Patrol mode names (now Ask first, Safe auto-fix, Autopilot). - Wrong values and behaviour: LOG_MAX_AGE=0 does not disable cleanup, Patrol evidence budget is 10 calls and 12 turns, ALLOW_UNPROTECTED_EXPORT covers private networks too, PULSE_AUDIT_ASYNC defaults on. - Agent install: install.sh ignores PULSE_ENABLE_KUBERNETES and PULSE_ENABLE_PROXMOX, rejects --disable-auto-update, and the token fallback lives in the state directory. - API: audit endpoints answer 402 on Community, onboarding QR is GET, type=container is rejected, several routes need admin plus a scope. - HTTP_REDIRECT_PORT was not documented anywhere. Docs are mirrored under frontend-modern/public/docs, so each change is applied to both copies. The tests that pinned the old wording (including three that asserted claims the code contradicts) are updated alongside. Contract-Neutral: Customer docs corrected to match shipped behaviour; no runtime, API or contract change.
13 KiB
Multi-Tenant Organizations (Enterprise/Internal)
Pulse supports shared-process organizations for Enterprise and internal multi-organization deployments. Each organization gets its own infrastructure, resources, alerts, and audit log namespace on the same Pulse process.
This is not the canonical Pulse MSP model for separate customer businesses. MSP crosses legal and security ownership boundaries, so the canonical MSP route is provider-hosted: a Stripe-free provider control plane runs one isolated Pulse runtime per client workspace. Use shared-process organizations when one owner is deliberately separating internal sites, teams, departments, or environments.
Requirements
| Requirement | Detail |
|---|---|
| Feature flag | PULSE_MULTI_TENANT_ENABLED=true |
| License | Enterprise license with multi_tenant capability |
Without these, all API calls return 501 Not Implemented (flag off) or 402 Payment Required (no license). The default organization always works regardless.
The Community, Relay, and Pro tiers do not include the multi_tenant capability, and Enterprise licensing is not sold self-serve on the pricing page. Email support@pulserelay.pro to arrange an Enterprise license.
Quick Start
- Set
PULSE_MULTI_TENANT_ENABLED=truein your environment and restart Pulse. - Activate your Enterprise license in Settings → Plans & Billing.
- Create the organization with the session-authenticated
POST /api/orgsendpoint (see Creating an Organization). This build has no Create Organization button in Settings → Organization. - Assign infrastructure to the organization.
- Use the Org Switcher in the header bar to switch between organizations.
Concepts
Organizations
An organization is a separate monitoring namespace inside the same Pulse runtime:
- Its own set of monitored nodes and resources.
- Its own alerts, thresholds, and notifications.
- Its own audit log.
- Its own configuration directory on disk.
The default organization always exists and is used when multi-tenant is disabled. It cannot be deleted or renamed.
Roles
Each member has a role within an organization:
| Role | Permissions |
|---|---|
| Owner | Full control. Can transfer ownership, delete the org. |
| Admin | Manage members, shares, and org settings. Cannot transfer ownership. |
| Editor | Read/write access to org resources. Cannot manage members or shares. |
| Viewer | Read-only access to all org data. |
Resource Sharing
Organizations can share specific resources with other organizations:
- Share a VM, container, machine agent, storage, PBS, or PMG resource with another org.
- Assign an access role (
viewer,editor, oradmin) to the share. - The receiving org must accept the share before it sees the resource alongside its own.
Before Using the API
Manage members and shares in the signed-in Pulse UI. Organizations are created through the session-authenticated POST /api/orgs endpoint; the UI has no create control.
These changes require session-based user authentication and the relevant
organization role; API tokens are rejected with 403 session_required, even
when they have settings:write. The UI handles the session and CSRF protection.
Do not extract a session cookie or CSRF token into a shell command.
The read-only examples use an org-bound token with settings:read. Define the
pulse_api helper and prepare its private header file from
API authentication in the same Bash session. This is
local example code, not an installed Pulse command.
Keep the token out of command lines, URLs and reports. A token's organization
binding is an access boundary: changing a URL or X-Pulse-Org-ID header does
not grant it access to another organization.
The helper requires curl 7.76 or later, ignores local trace/verbose defaults, and applies a five-second connection limit and twenty-second whole-request limit. It does not follow redirects or retry. It prints only the HTTP status and the path to a new owner-only file containing the response, including on HTTP failure. Member identities, resource details and error bodies stay out of terminal output; open the file privately and share only a relevant redacted excerpt, not whole member, share or infrastructure responses. A partial file after a transport failure is not a complete list or proof that no members or shares exist. HTTP 401/402/403 and redirects are not successful reads; do not widen a token or use a session cookie to bypass them.
The helper's loopback origin applies on the Pulse host; remotely, follow its
HTTPS instructions and keep certificate verification enabled. Replace the
example organization ID production-datacenter with your own, and run each
read separately. The helper's support for other HTTP methods does not make
organization mutations token-authenticated; keep member and share changes in the
signed-in UI with its session and CSRF protection.
Managing Organizations
Creating an Organization
UI: none in this build; Settings → Organization has no create control.
API contract: POST /api/orgs, session authentication only:
{"id": "production-datacenter", "displayName": "Production Datacenter"}
The creator becomes the owner. id is a stable ID of 1–64 characters using
letters, digits, periods, underscores or hyphens, but not . or ..; use a
simple lowercase/hyphen ID such as the example. displayName is the name
shown in Pulse. name and description are not the creation fields.
Switching Organizations
Use the Org Switcher dropdown in the header. When you switch:
- All pages reload with the new organization's data.
- AI chat history is reset (each org has its own context).
- Caches are invalidated and re-fetched.
Managing Members
UI: Settings → Organization → Access
Read-only API:
pulse_api GET /api/orgs/production-datacenter/members
Invite or update a member: use the Access panel as an owner or admin.
Its API contract is POST /api/orgs/{id}/members, session authentication only:
{"userId": "user-id", "role": "editor"}
For a new member, the response is 202 with a pending invitation; the user
must accept it in Pulse before gaining access. An existing member's role is
updated by posting their userId and new role to the same endpoint, not by
PATCHing a member URL:
{"userId": "user-id", "role": "admin"}
Only the current owner can transfer ownership, and only to an existing member after fresh sign-in. The owner cannot be demoted or removed as an ordinary member update. Default-organization members cannot be managed here.
Sharing Resources
UI: Settings → Organization → Sharing
Create the share as an owner or admin of the source organization. An owner or admin of the target organization then accepts it in Sharing → Incoming. Until acceptance, the share is pending and does not grant access.
API contract: POST /api/orgs/{id}/shares, session authentication only:
{
"targetOrgId": "other-org-id",
"resourceType": "vm",
"resourceId": "vm:101",
"accessRole": "viewer"
}
Use accessRole, not role, and the resource type and ID returned by Pulse,
not a display name. Supported types include vm, system-container, agent,
node, docker-host, storage, pbs and pmg; the generic types host and
container are not supported. Changing a share's access role makes it pending
again, so the target must accept the new grant.
Read-only incoming shares:
pulse_api GET /api/orgs/production-datacenter/shares/incoming
Monitoring Multiple Internal Estates
An Enterprise deployment can run one central Pulse server and keep each internal estate in its own organization, so dashboards, alerts, notifications, and audit logs are scoped by organization. The same default node names (pve, pve1) in different organizations do not collide, because each organization is a separate namespace.
Use this for one company operating many internal sites, teams, departments, or environments. Do not use this as the default MSP model for unrelated customer businesses; MSP client isolation belongs to the provider-hosted client-workspace model with one isolated Pulse runtime per client.
To onboard an internal estate:
- Create an organization for the estate (see Creating an Organization).
- Create an org-bound API token with the
agent:reportscope, bound to that estate's organization (orgId). A token bound to a single organization automatically routes every agent that uses it into that organization, with no extra header required. Binding also scopes the token: an org-bound token cannot access other organizations, including the default org (binddefaultexplicitly if a token genuinely needs it). Legacy unbound tokens keep their default-org access. - Install the estate's agents (Proxmox host, Docker, Kubernetes) using that token. Their telemetry lands in the selected organization.
- (Optional) Alias node names per estate. If two estates both use the default
pvehostname and you want them visually distinct, set--hostname(or thePULSE_HOSTNAMEenvironment variable) on the agent, for example--hostname "acme-pve1". See UNIFIED_AGENT.md. - (Optional) Isolate the agent control plane on its own port. When remote nodes reach the central server across the internet, enable Split-Port Agent Ingest so reports and command WebSockets share a dedicated, firewalled agent port that never exposes the web UI or management API.
Route each estate's alerts into the right internal system with per-organization webhooks or the org-scoped alerts API. See the multi-tenant section of WEBHOOKS.md.
Licensing: self-hosted multi-tenant requires an Enterprise license with the multi_tenant capability (see Requirements). MSP licensing is separate and is based on a signed provider MSP license that sets the client workspace cap for isolated client runtimes, not shared-process organizations.
Settings Panels
When multi-tenant is enabled, Settings → Organization shows:
| Panel | Description |
|---|---|
| Overview | Organization display name (editable), org ID, creation date, member count |
| Access | Member list, invite/remove members, change roles |
| Sharing | Outgoing and incoming resource shares |
| Billing | Organization plan, usage policies and subscription status (the "Billing & Usage" panel) |
API Reference
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/orgs |
List organizations the current user can access |
POST |
/api/orgs |
Create a new organization |
GET |
/api/orgs/{id} |
Get organization details |
PUT |
/api/orgs/{id} |
Update organization (session only) |
DELETE |
/api/orgs/{id} |
Delete organization |
GET |
/api/orgs/{id}/members |
List members |
POST |
/api/orgs/{id}/members |
Invite a member or update an existing member's role (session only) |
DELETE |
/api/orgs/{id}/members/{userId} |
Remove a member |
POST |
/api/org-invitations/{id}/accept |
Accept your pending invitation (session only) |
GET |
/api/orgs/{id}/shares |
List outgoing shares |
GET |
/api/orgs/{id}/shares/incoming |
List incoming shares |
POST |
/api/orgs/{id}/shares |
Create or update a share (session only) |
POST |
/api/orgs/{id}/shares/incoming/{shareId}/accept |
Accept an incoming share (session only) |
DELETE |
/api/orgs/{id}/shares/{shareId} |
Remove a share |
Tenant Context
All data-fetching endpoints respect the active organization context. The active org is determined by:
X-Pulse-Org-IDheader (API clients)- Session cookie (browser)
- Falls back to the
defaultorganization
Neither the active context nor a token scope overrides organization membership or a token's organization binding.
Storage
- The default org uses the root data directory (backward compatible).
- Non-default orgs store data in
{data-dir}/orgs/{org-id}/. - Organization metadata is stored in
org.jsoninside each org directory. - When multi-tenant is first enabled, legacy single-tenant data is migrated into
orgs/default/with symlinks for compatibility.
Troubleshooting
"Multi-tenant functionality is not enabled. Set PULSE_MULTI_TENANT_ENABLED=true to enable." (501)
Set PULSE_MULTI_TENANT_ENABLED=true in your environment and restart Pulse.
"Multi-tenant access requires an Enterprise license" (402)
Activate an Enterprise license with the multi_tenant capability in Settings → Plans & Billing.
Organization data not loading after switch
- Hard-refresh the browser (
Ctrl+Shift+R). - Check the Org Switcher dropdown — ensure the correct org is selected.
- Check Pulse logs for tenant middleware errors.
Shared resources not appearing
- Verify the share exists and has been accepted: Settings → Organization → Sharing → Incoming.
- Confirm the share role grants sufficient access.
- Check that the source org's resources are online.
See Also
- Plans & Entitlements, multi-tenant availability by plan
- Pulse Cloud, hosted Pulse environment
- Security, authentication and authorization model