An identity can be a member of any project with some role and optionally variables assigned. This is called a membership. A project admin (and a superadmin) can manage project memberships by default, you can also setup Tenant ACL permissions for other user roles.

Creating a project membership

Add an existing identity to a project. For this operation you need to know an identity ID. If you want to add a user by an email check invite mutation

mutation {
  addProjectMember(
    projectSlug: "my-blog" 
    identityId: "2f673a53-af33-42b1-9e17-e1305fa26d9d"
    memberships: [
      {
        role: "editor",
        variables: [{name: "language", values: ["cs"]}]
      }
    ]
  ) {
    ok
    error {
      code
    } 
  }
}

a mutation can fail with following errors:

enum AddProjectMemberErrorCode {
  PROJECT_NOT_FOUND
  IDENTITY_NOT_FOUND
  VARIABLE_NOT_FOUND
  ALREADY_MEMBER
}

Updating a project membership

You can update existing member of an project using updateProjectMember mutation. Arguments and response structure of the mutation are the same as of addProjectMeber mutation. Only error codes differs:

enum UpdateProjectMemberErrorCode {
  PROJECT_NOT_FOUND
  VARIABLE_NOT_FOUND
  NOT_MEMBER
}

Revoking a project membership

You can remove a project member using following mutation

mutation {
  removeProjectMember(
    projectSlug: "my-blog" 
    identityId: "2f673a53-af33-42b1-9e17-e1305fa26d9d"
  ) {
    ok
    error {
      code
    } 
  }
}

Viewing project members

query {
  projectBySlug(slug: "my-blog") {
    name
    members {
      identity {
        id
        person {
          email
        }
      }
      memberships {
        role
        variables {
          name
          values
        }
      }
    }
  }
}

Listing persons across the tenant

Available since 2.2

projectBySlug { members } lists the members of one project. To list persons across the whole tenant — for a user-management UI — use the top-level persons query:

query {
  persons(filter: { email: "jane" }, limit: 50, offset: 0) {
    id
    email
    name
    otpEnabled
    emailOtpEnabled
    identity {
      id
      roles
    }
  }
}

filter (all fields combined with AND, all optional) matches by email (case-insensitive substring), personId, or identityId. limit / offset paginate; the page size is capped server-side (default 100, max 1000), so an unbounded limit can't dump the whole table.

Visibility is enforced per caller and gated by the person:list permission:

  • SUPER_ADMIN (which holds person:list by default) sees every person.
  • Anyone else sees exactly the members reachable via project.members — for each project the caller may view members of, the persons behind the visible memberships, with the same per-role filtering that project.members applies. So a PROJECT_ADMIN sees only their projects' members.
  • A caller who may not list anyone gets an empty list rather than an error.

The projection deliberately omits password hashes and TOTP secrets. See two-factor for the otpEnabled / emailOtpEnabled MFA flags.

Global identity roles

Project memberships scope a person's access to a single project. Global roles (super_admin, project_admin, custom global roles defined in the tenant ACL — see Tenant ACL permissions) are attached directly to an identity and are not project-scoped. Two mutations grant and revoke them:

mutation {
  addGlobalIdentityRoles(
    identityId: "2f673a53-af33-42b1-9e17-e1305fa26d9d",
    roles: ["super_admin", "monitor"]
  ) {
    ok
    error { code developerMessage }
    result { identity { id roles } }
  }
}
mutation {
  removeGlobalIdentityRoles(
    identityId: "2f673a53-af33-42b1-9e17-e1305fa26d9d",
    roles: ["monitor"]
  ) {
    ok
    error { code }
    result { identity { id roles } }
  }
}

Errors:

CodeCause
IDENTITY_NOT_FOUNDNo such identity.
INVALID_ROLEA role in the list is not defined in the tenant ACL or the caller is not allowed to grant it.

Both are gated by identity:addGlobalRoles / identity:removeGlobalRoles against the role list — SUPER_ADMIN can grant any role, PROJECT_ADMIN is restricted to the project-admin allowed-input-roles set, and any other identity needs an explicit ACL grant scoping which roles it may assign. result.identity.roles echoes the new role set after the mutation.

Audit

Available since 2.2

Every successful addProjectMember, updateProjectMember, and removeProjectMember is recorded in the audit log as project_membership_create / project_membership_update / project_membership_remove, with a {before, after} snapshot of the affected memberships and target_person_id resolved from the affected identity.

Global role mutations (addGlobalIdentityRoles, removeGlobalIdentityRoles) are audited the same way as global_role_grant / global_role_revoke.