S
supero.docs
Documentation/Managing Your App/Roles & Permissions Reference

Roles & Permissions Reference

The real, closed set of Supero roles — hierarchy, scope, who can assign what, custom-role relabels, permission strings, and every sign-in/account flow.

Overview

Every user and API key in Supero carries exactly one role from a fixed, closed set — there is no free-form role string. This page is the reference for that role catalog: what each role can see, which UI surface it lands on, who can assign it, how custom role names work, and the API/SDK calls you use to manage all of it.
For the deeper mechanics of how permissions are enforced per request — including the separate object-level access_policy layer that scopes individual entities, records, and fields — see the Access Control (RBAC) page. This page focuses on the role catalog itself and the account/sign-in flows that put a role on a session.
http
POST https://api.supero.dev/api/v1/auth/login
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "secret123",
  "domain": "acme-corp"
}
The login response stamps the resolved role and its full permission list onto the session — that's what every example below builds on.

The Built-In Role Hierarchy

Roles are strictly hierarchical. A lower level number means more privileged, and — as covered in the next section — a role can only ever be assigned to a level strictly below the assigner's own.
RoleLevelScopePurpose
domain_admin1domainOwns the domain: creates and manages every project, tenant, user, and schema in it
domain_user2domainDomain-wide read plus full CRUD across every project/tenant; no admin actions
project_admin3projectManages every tenant in one project; the role that defines and edits schemas
project_user4projectProject-wide read plus full CRUD across every tenant in the project; no admin actions
developer5projectTechnical role: schema, SDK and feature-flag management, plus CRUD inside its own tenant for building and testing. Despite the project-level name it is not cross-tenant.
tenant_admin6tenantManages one tenant's users and settings; can read schemas but not define them
tenant_user7tenantRegular business user: full CRUD inside their own tenant, no admin actions
viewer8tenantRead-only access inside their tenant

⚠️ platform_admin / platform_user are not customer-assignable

platform_admin (level 0, global scope) and platform_user (an identical clone kept as a separate label for staff accounting) sit above domain_admin. They are Supero-staff-only roles — do not offer them in your own app's role picker, and expect assignment attempts against them to be rejected for any non-staff caller.
A user's role is resolved at login into base_role — for a built-in role this is just the role itself; for a custom role (covered below) it's the built-in role the custom name inherits from. Every access decision downstream (permission checks, UI surface, object-level policy) is made against base_role, never the raw role string.

Admin Shell vs. Customer Surface

At login, Supero decides which top-level surface a role should render and returns it as user.ui_view (mirrored at context.surface): 'admin' for the sidebar app-shell and dashboard widgets, or 'customer' for the storefront/public-style experience. Apps read client.userInfo.ui_view (or call client.isCustomer() / client.isAdmin()) at boot time instead of matching role strings by hand.
ui_view = adminui_view = customer
domain_admin, project_admin, tenant_admin, developerdomain_user, project_user, tenant_user, viewer

⚠️ domain_user and project_user render the customer surface

Despite having broad, cross-tenant read+CRUD access, domain_user and project_user are NOT in the admin-surface set — they land on the same customer-facing UI as tenant_user and viewer. If you're building an internal analyst tool on top of one of those roles, don't assume the admin shell will render for it automatically.
This is a superset relationship: every role that can manage users or schemas is in the admin set, plus developer (a technical role that still needs the admin panel to manage schemas and SDKs, even though it can't manage users).

Who Can Assign Which Role

A caller can only assign a role strictly less privileged than their own — i.e. a strictly higher hierarchy level. A project_admin (level 3) can assign project_user, developer, tenant_admin, tenant_user, or viewer, but never another project_admin, and never anything at domain level or above. This is enforced on every role-carrying write, not just in the dashboard.

Create a user with a role

http
POST https://api.supero.dev/api/v1/users/register
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "domain_name": "acme-corp",
  "email": "[email protected]",
  "password": "SecurePass123!",
  "tenant_uuid": "<tenant-uuid>",
  "role": "tenant_admin"
}

Change an existing user's role

http
PUT https://api.supero.dev/api/v1/users/<user_uuid>/role
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "role": "tenant_user"
}
Both endpoints reject the write with 403 if the caller's role can't manage the target role, and both resolve custom role names to their base_role before scoring the ceiling, so a custom-named role is checked at the privilege level of whatever it inherits from.

Python SDK

python
import supero

org = supero.login(domain_name='acme-corp', email='[email protected]', password='secret123')

# Create a user with an explicit role
user = org.add_user(
    email='[email protected]',
    password='SecurePass123!',
    name='Bob',
    role='tenant_admin',
)

# Change an existing user's role
org.users.assign_role(user['user_uuid'], 'tenant_user')

Custom Roles Are Relabels, Not New Privilege Levels

You can register a custom role name — "dentist", "patient", "agent" — so your schemas and UI can use domain-specific labels. A custom role adds zero new permissions: it's a thin registry record that points at a base_role, and every real access decision (permissions, ui_view, object-level policy) is made against that base_role, not the custom name.
json
{
  "parent_type": "project",
  "parent_uuid": "<project-uuid>",
  "name": "dentist",
  "display_name": "Dentist",
  "description": "Clinic staff with full tenant access",
  "base_role": "tenant_admin"
}
http
POST https://api.supero.dev/api/v1/crud/acme-corp/role
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "parent_type": "project",
  "parent_uuid": "<project-uuid>",
  "name": "dentist",
  "display_name": "Dentist",
  "description": "Clinic staff with full tenant access",
  "base_role": "tenant_admin"
}

🚨 base_role is capped at tenant level

base_role must be one of tenant_admin, tenant_user, or viewer — that's the hard privilege ceiling for every custom role. A custom role can never resolve into a platform-, domain-, or project-level base_role, no matter what name you give it. You also can't shadow a built-in role name, and a caller can't create a custom role whose base_role outranks their own.
Assign a custom role exactly like a built-in one — pass its name as role on the register/assign-role calls above. At login (and on session refresh / /auth/me), Supero resolves it to its base_role automatically, so "dentist" gets exactly tenant_admin's permissions and admin-surface treatment while your UI keeps showing "Dentist" everywhere.

Permission Strings & Checking Access in Code

Every role also maps to a flat list of resource:action permission strings — the coarse layer that decides whether a role can touch a resource type at all. This list is resolved once at login (and again on session refresh) and shipped as user.permissions; it's what your UI should gate on instead of matching role strings.
CategoryExample strings
crudcrud:create, crud:read, crud:update, crud:delete, crud:list
schemaschema:manage, schema:create, schema:read, schema:update, schema:delete, schema:list
tenant / project / usertenant:create, tenant:list, project:update, user:create, user:move
serviceservice:import, service:configure, service:execute, service:list
access_policy / roleaccess_policy:create, access_policy:update, role:create, role:list
api_key / feature_flagapi_key:create, api_key:revoke, feature_flag:update
In a generated app's frontend, the runtime client exposes gating helpers that read strictly from what the server shipped — never re-derived client-side:
javascript
// Coarse permission-string check (Layer 1)
if (client.has('crud:delete')) {
  // show the delete button
}

// Per-entity object-level check (Layer 2 — see Access Control (RBAC))
if (client.can('update', 'appointment')) {
  // allow editing this record type
}

// Surface / role helpers
if (client.isAdmin()) { /* render admin shell affordances */ }
if (client.isCustomer()) { /* render storefront affordances */ }

⚠️ Client checks gate UI, never authorization

has()/can()/isAdmin() only control what your UI renders. Every request is re-checked against the same permission list and object policy server-side, so a stale or tampered client-side check can't grant access it shouldn't have.

Sign-In & Account Flows

All of these mint the same kind of session — a resolved role, permission list, and JWT pair with an 8-hour access token and a 7-day refresh token — regardless of which flow was used to get there.
FlowHow it works
Password loginPOST /api/v1/auth/login with email, password, and optionally domain/project/tenant hints. If domain is omitted, Supero looks up the caller's registered domain automatically.
Google Sign-InThe frontend fetches config from GET /api/v1/auth/google/config, redirects to Google, then exchanges the returned code via POST /api/v1/auth/google/login {auth_code, redirect_uri}. Returns action: "login" with a full session for an existing user, or action: "register" with the Google profile if the email is new.
Okta / OIDC SSOConfigured as a managed integration in the services catalog by a domain admin. Once connected, users authenticate through your identity provider and Supero establishes the session for their verified email — no password is ever stored in Supero for SSO accounts.
Self-serve sign-upPOST /api/v1/auth/register with email, password, and domain_name sends an emailed OTP; POST /api/v1/auth/verify-email {email, code} completes registration and returns a session. POST /api/v1/auth/resend-otp re-sends the code.
Admin inviteAn admin creates the account via POST /api/v1/users/register with require_activation: true — the user is created disabled and emailed an OTP. The invitee completes activation with POST /api/v1/auth/activate-account {email, domain, code} (this does not itself create a session — they log in normally afterward). POST /api/v1/auth/resend-activation re-sends the invite code.
Password resetPOST /api/v1/auth/forgot-password {email} starts the flow; POST /api/v1/auth/verify-reset-otp {email, otp} returns a reset_token; POST /api/v1/auth/reset-password {reset_token, new_password} completes it.

ℹ️ Refreshing and ending a session

POST /api/v1/auth/refresh exchanges a refresh_token for a new access_token before the 8-hour access token expires. POST /api/v1/auth/logout revokes the current session.

Next Steps

  • •Access Control (RBAC) — the object-level access_policy layer that scopes individual entities, records, and fields on top of the role catalog described here
  • •Users & Roles — dashboard and API walkthroughs for day-to-day user administration
  • •Projects & Tenants — how the domain/project/tenant hierarchy that roles scope against is structured
  • •Building Multi-Tenant Apps — patterns for using roles and tenant scope together in your own app