Last updated 2026-06-29

Groups & Nesting

This guide models reusable membership sets — teams, distribution lists, "family", "all-engineering" — with the built-in group namespace, and shows how groups nest and how granting a group access to anything works.

What you'll learn: how to add users and other groups to a group, how to grant a group a role or a resource permission with a single userset tuple, and how a Check resolves transitively through nested groups.

The group namespace

A group has exactly one relation, and it is a plain leaf — just the directly stored tuples:

member := this()

There is no role hierarchy inside a group; it is a flat set. The power comes from what a member can be: a subject is either a concrete user or a userset — and a userset may itself point at another group's member relation. That is how groups nest.

Adding members

Use GroupService.AddGroupMember. Its member field is a oneof: pass user_id to add a person, or group_id to nest a whole group. Like all management RPCs it requires acting_user_id.

# add a user to the backend team
curl -X POST http://localhost:8080/workspace.v1.GroupService/AddGroupMember \
-H "Authorization: Bearer $WS_SERVICE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"group_id":"backend","member":{"user_id":"alice"},"acting_user_id":"alice"}'

That stores a tuple with a concrete-user subject:

group:backend#member@alice

Nesting a group inside a group

To make every member of backend also a member of the broader all-eng group, add backend as a group member of all-eng:

curl -X POST http://localhost:8080/workspace.v1.GroupService/AddGroupMember \
-H "Authorization: Bearer $WS_SERVICE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"group_id":"all-eng","member":{"group_id":"backend"},"acting_user_id":"alice"}'

The stored tuple uses a userset subject:

group:all-eng#member@group:backend#member

Read that as "everyone who is a member of backend is a member of all-eng." Nesting is unbounded — all-eng can in turn be nested inside everyone. Traversal is cycle-safe, so an accidental loop cannot hang a check.

Granting a group access to anything

Because any subject may be a userset, you grant a whole group access to an object by writing one tuple whose subject is the group's member set. The same shape works across every namespace:

# the family group can view a document
resource:doc#viewer@group:family#member
# the all-eng group are members of the acme workspace
workspace:acme#member@group:all-eng#member

These are written through AuthzService.WriteRelationTuples (one or more INSERT/DELETE ops in a single call):

curl -X POST http://localhost:8080/workspace.v1.AuthzService/WriteRelationTuples \
-H "Authorization: Bearer $WS_SERVICE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"updates":[
{"op":"INSERT","tuple":{
"namespace":"workspace","object_id":"acme","relation":"member",
"subject":{"set":{"namespace":"group","object_id":"all-eng","relation":"member"}}}}
]}'

Now every member of all-eng — including the nested backend members — is a member of acme, with no per-user tuples. Add or remove a person from backend once and their access everywhere those groups are granted updates automatically.

A transitive Check

Given the tuples above:

group:backend#member@alice
group:all-eng#member@group:backend#member
workspace:acme#member@group:all-eng#member

Ask whether alice is a member of the acme workspace:

curl -X POST http://localhost:8080/workspace.v1.AuthzService/Check \
-H "Authorization: Bearer $WS_SERVICE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"namespace":"workspace","object_id":"acme","relation":"member","subject_user_id":"alice"}'
{ "allowed": true }

Check walks the chain:

  1. workspace:acme#member has a userset subject group:all-eng#member — recurse into Check(group, all-eng, member, alice).
  2. group:all-eng#member has a userset subject group:backend#member — recurse into Check(group, backend, member, alice).
  3. group:backend#member@alice is a stored tuple with a concrete subject — the leaf matches. Allowed.
Groups are not workspaces. A group is a pure membership set with no roles and no ownership of resources; it exists to be referenced by grants. Keep "who is on the team" (groups) separate from "what the team can do" (the tuples that grant the group access). See ADR-0003.

Where to next