Getting Started

Tenant User Administration

Invite users, assign roles, and manage your team's access to Versus from an in-app Tenant Admin console — backed by a dedicated identity organization per tenant, with server-side isolation so one tenant's admin can never touch another's users.

Every Versus tenant is its own isolated identity organization. As a tenant admin you manage your team — invite people, set what each of them can do, deactivate someone who leaves — from an in-app Tenant Admin console, without ever touching the underlying identity provider's raw admin screens. This page explains what that console is, how it's isolated, and how you use it day to day.

The model is deliberately simple: one identity organization per tenant, one-to-one with your Versus tenant. Your users, their roles, and every administrative action are confined to that organization. No forgotten header, mistyped ID, or API call can enumerate or change another tenant's users.


The model at a glance

  • Your tenant is an identity organization. When your tenant is provisioned, Versus creates a dedicated organization for it on the Versus identity provider (IdP) and links it to your tenant record. That organization is the boundary for your users, their sign-in credentials, and their roles.
  • Two directory modes, one experience. How your users' credentials are sourced differs by tenant kind, but the admin experience is identical:
    • Versus IdP (local users) — the default for demo tenants. Users are created directly in your organization on the Versus IdP; they sign in with email + password/passkey and mandatory MFA. No external IdP required.
    • Client OIDC / Client SAML — for production tenants that federate their own corporate IdP (Okta, Entra, etc.) upstream into their organization. The client IdP owns credentials and user lifecycle; Versus speaks one protocol (OIDC) regardless.
  • The tenant admin is a delegated org manager, surfaced in the product as the tenant_admin role. You administer users through the Versus console, which proxies the IdP's organization-scoped management API on your behalf — you never see the IdP's own console.

For where this sits in the broader stack, see Platform Architecture.


Roles

Versus reuses its platform RBAC — there is no separate tenant role system. The Tenant Admin console can assign exactly three roles:

RoleLevelWhat it grants
Viewer2Read-only access.
Operator3Day-to-day operational changes; no admin.
Tenant Admin4Full admin over the tenant and access to the Tenant Admin console.

Assigning a role creates a grant for the user on the shared Versus project in your organization. A user holds one Versus role at a time — assigning a new role replaces the previous grant. The role travels in the user's sign-in token; on their next token refresh the platform reflects the new level, mapping tenant_admin onto the internal network_admin (level 4) admin floor.

super_admin (level 5) is never grantable here. It is a Versus-staff-only role that exists solely in the platform's own organization and is guarded by a separate staff allowlist. It does not appear in the console's assignable set.

Who may administer

The console is protected by a two-part check. To reach it you must be both:

  1. at RBAC level ≥ 4 (admin), and
  2. flagged as a tenant admin for the tenant your token was minted for.

The tenant is read from your authenticated session's token claim — never from anything the client sends — so a network admin of one tenant cannot administer another. The tenant-admin flag itself is kept in sync automatically from the tenant_admin role in your token each time you sign in.


Using the console

The console lives under your tenant's subdomain at Admin → Tenant Users. It lists everyone in your organization with their email, role, and status, and lets you invite, re-role, deactivate/reactivate, and remove users.

Under the hood the UI calls a small REST surface. Every endpoint resolves your organization server-side from your session and binds each management-API call to it.

Console context

GET /api/tenant-admin/meta/

Returns the assignable roles and your tenant context (org code, name, kind, directory mode), plus whether identity administration is wired up for this deployment:

{
  "tenant": { "org_code": "acme", "org_name": "Acme Corp",
              "kind": "demo", "idp_mode": "versus_idp" },
  "roles": [
    { "key": "viewer",       "label": "Viewer",       "level": 2 },
    { "key": "operator",     "label": "Operator",     "level": 3 },
    { "key": "tenant_admin", "label": "Tenant Admin", "level": 4 }
  ],
  "identity_admin_configured": true
}

List users

GET /api/tenant-admin/users/
{
  "count": 2,
  "results": [
    { "id": "2891…", "username": "[email protected]", "email": "[email protected]",
      "first_name": "Jane", "last_name": "Smith", "state": "ACTIVE",
      "role": "tenant_admin" }
  ]
}

Invite a user

POST /api/tenant-admin/users/
Content-Type: application/json

{ "email": "[email protected]", "first_name": "Sam",
  "last_name": "Lee", "role": "operator" }
201 Created
{ "id": "3902…", "email": "[email protected]", "role": "operator" }

Only email is required; role defaults to viewer. The invited user is created in your organization in a passwordless, invitable state and assigned the role you chose. Onboarding then happens entirely on the IdP: the invitee follows a branded email link through the hosted login flow to verify their email, set a password (or passkey), and enroll MFA, and lands back in Versus. Versus itself never handles the credential — it remains a pure relying party.

Change a role

POST /api/tenant-admin/users/{id}/role/
{ "role": "tenant_admin" }

The role must be one of viewer, operator, tenant_admin; anything else is rejected.

Deactivate, reactivate, and remove

POST   /api/tenant-admin/users/{id}/deactivate/
POST   /api/tenant-admin/users/{id}/reactivate/
DELETE /api/tenant-admin/users/{id}/

Deactivating blocks sign-in while preserving the account; removing deletes the user from the organization entirely.


How isolation works

The console never trusts client input for which organization it acts on:

  • Your session token carries your tenant. The backend resolves that tenant to its linked identity organization and sends every management-API call scoped to that organization ID.
  • The organization ID is derived server-side — a value in a request body or header claiming a different org is ignored.
  • The credential the backend uses is a delegated machine service account with a tight, organization-scoped set of manager rights: it can manage users and their role grants, but cannot change organization-wide security policy. Least privilege by design.

Because the service account is already organization-scoped and the organization is derived from your session, a tenant admin's reach is structurally confined to their own tenant. This mirrors the platform's data-plane isolation, where each tenant's records and secrets are separated by row-level security and per-tenant encryption keys — see Security & Compliance.

Every administrative write is recorded twice: once in the Versus application audit trail and once, independently, by the identity provider — two trails for the same action.


Configuration states you may see

Identity administration is config-gated, so the console degrades cleanly rather than erroring when a deployment hasn't wired it up:

StateMeaning
Not configured (503)The deployment has no identity-admin credential set. The console shows a banner and the Invite button is disabled.
No linked organization (409)The credential is set, but this tenant hasn't been linked to an identity organization yet — usually mid-provisioning.
Upstream error (502)The identity provider rejected a call; the message is safe to surface and retry.

On a single-tenant, self-hosted appliance this whole layer is inert by design: with no identity-admin credential configured, the existing local sign-in path is unchanged and the console simply reports "not configured."


Where organizations come from

You don't create the identity organization yourself — it's part of tenant provisioning. A platform operator starts it from Control Plane → New Tenant (/control-plane/new-tenant: name, slug, subdomain, requester, module entitlements) or by approving a public tenant request; either way the provisioning workflow then:

  1. Creates the tenant's identity organization and links it to the tenant record.
  2. Grants the shared Versus project to the organization so its users can hold viewer / operator / tenant_admin.
  3. Bootstraps the first tenant admin from the requester's email (assigned tenant_admin) and adds any additional invitees as viewer.
  4. Emails each provisioned user their native invitation to onboard through the IdP.

These steps are reversible and compensating — a failure part-way rolls back the partial organization — and a tenant purge deletes the identity organization (users, grants, and service account) alongside crypto-shredding the tenant's secrets. Once the tenant is live, you invite everyone else from the console.

Demo vs. production

  • Demo tenants use local users on the Versus IdP. The tenant admin has the full lifecycle: invite, role, deactivate, remove — no external IdP anywhere in the picture.
  • Production tenants federate the client's own IdP. Credentials and user lifecycle (including offboarding) are owned by that IdP; in the console the user list is read-mostly and the writable surface is role assignment and app-level enable/disable. Local user creation is disabled except for a documented break-glass admin used only for IdP-outage recovery.

Promoting a demo tenant to production keeps the same organization — the client IdP is attached to it — so there is no re-onboarding of the team, only a change in where credentials come from.


Related