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
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 holdsperson:listby 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 thatproject.membersapplies. So aPROJECT_ADMINsees 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:
| Code | Cause |
|---|---|
IDENTITY_NOT_FOUND | No such identity. |
INVALID_ROLE | A 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
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.