Last updated 2026-06-29
ADR-0003 — Groups as a distinct nestable namespace, not a workspace sub-type
Status
Accepted (2026-06-15). Builds on ADR-0001 (relation tuples) and ADR-0002 (workspaces). This ADR decides how reusable membership sets — email lists, chat groups, on-call rotations — are modelled.
Context
The motivating products all need named, reusable sets of people that are not tenancy boundaries:
- The workplace tool has email distribution lists, chat channels, and on-call rotations (
all-engineering,sre,incident-responders). - The personal-assistant user has a
familylist they share many different tasks with. - A learning company has cohorts and teaching-assistant groups.
These sets share three properties that a workspace does not have:
- They are reused across many objects.
sreshould grant access to dozens of incidents without being re-declared each time. - They nest.
all-engineeringcontainsbackendandfrontend; adding a person tobackendshould propagate. - They are not an ownership/billing boundary. A workspace has one owner and is where resources live; a group is just a membership set.
The temptation is to model a group as a special workspace or a fixed sub-list inside one workspace. Both are wrong: a workspace is single-owner and tenancy-scoped (ADR-0002), and a group that lives inside one workspace cannot be referenced from another, defeating reuse.
Decision
Model groups as their own first-class namespace, group, distinct from workspace.
groupis a separate namespace with relationmember. Its rewrite rule isthis, and a member may itself be a userset, so groups nest:group:all-eng#member@group:backend#member. AGroupMemberis aoneofof auser_idor a nestedgroup_id.- Granting a group access is one tuple. Because a tuple's subject can be a userset, granting a group access to anything is
…#<relation>@group:<id>#member:workspace:acme#member@group:all-eng#member # all engineers are acme membersresource:incident-7#editor@group:sre#member # on-call can edit the incidentresource:task-buy-milk#viewer@group:family#memberCheckresolves the group transitively, including nested groups, with no special-casing in the consuming product. - Groups are referenceable across workspaces. A group may be project-level (empty
workspace_id— a B2C user'sfamilylist) or scoped to a workspace (a company's internal groups). Either way it is referenced by id from any workspace or resource tuple; scoping is an organisational convenience, not a reuse boundary.
Like every other RPC here, GroupService is reached at
/workspace.v1.GroupService/<Method> with the service
credential, and management calls carry acting_user_id as data:
# Nest the backend group inside 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"}'Consequences
- Positive. One group, many grants.
sreis defined once and granted across every incident; membership changes propagate everywhere via the userset, with no re-grant. - Positive. Nesting is free — it is the same userset mechanism ADR-0001 already provides.
- Positive. Clean separation of concerns:
workspaceis the ownership/tenancy boundary,groupis the membership set. - Negative. There are now two membership concepts and contributors must pick the right one. The rule of thumb: if it owns resources and bills, it is a workspace; if it is a named set of people you grant access with, it is a group.
- Negative / cycle risk. Nested groups can in principle form a cycle. The engine must bound
Checktraversal (visited-set / depth limit) so a malformed nesting cannot loop; this is an engine-level guard rather than a schema constraint.
Future work — ADR-0004
These three ADRs cover the authorization model. The
service-to-service authentication decision — that this is an
internal service called by trusted product backends with a shared
Authorization: Bearer credential
(GATEWAY_SERVICE_AUTH_TOKENS), with the end user passed as data
(acting_user_id / subject_user_id) rather than
inferred from the caller — deserves its own record. A future
ADR-0004 should capture that contract: why end-user JWT
verification lives at the product edge and not here, the mesh/mTLS mode
(empty token list), and the trust boundary between this service and its
callers. It is noted here as a placeholder; the decision itself is documented
under Security & Service Auth
until the ADR is written.