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.
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.
Versus reuses its platform RBAC — there is no separate tenant role system. The Tenant Admin console can assign exactly three roles:
| Role | Level | What it grants |
|---|---|---|
| Viewer | 2 | Read-only access. |
| Operator | 3 | Day-to-day operational changes; no admin. |
| Tenant Admin | 4 | Full 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.
The console is protected by a two-part check. To reach it you must be both:
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.
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.
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
}
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" }
]
}
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.
POST /api/tenant-admin/users/{id}/role/
{ "role": "tenant_admin" }
The role must be one of viewer, operator, tenant_admin; anything else is rejected.
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.
The console never trusts client input for which organization it acts on:
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.
Identity administration is config-gated, so the console degrades cleanly rather than erroring when a deployment hasn't wired it up:
| State | Meaning |
|---|---|
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."
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:
viewer / operator / tenant_admin.tenant_admin) and adds any additional invitees as viewer.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.
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.