Last updated 2026-06-29
Workspaces & Groups
This page explains the two membership primitives this service exposes and why
they are deliberately separate. A workspace is a
tenancy / ownership boundary that people belong to at a role; a
group is a reusable, nestable set of subjects you grant
access with. WorkspaceService manages the
workspace namespace; GroupService manages the
group namespace (see
Authorization Model).
Every management call carries acting_user_id
(the user the action is authorized as — required; omitting it returns
InvalidArgument) and an optional project_id (empty =
the default project). The caller is a trusted product backend, not the end
user — see Security & Service Auth.
Personal vs. team workspaces
The product models the full B2C↔B2B spectrum with two workspace kinds, the way Claude and ChatGPT do (see ADR-0002).
PERSONAL | TEAM | |
|---|---|---|
| How it appears | Auto-provisioned by ListWorkspaces on first call | Created by CreateWorkspace |
| Membership | Exactly one member — the owner | Many members, each at a role |
| Add members | No — AddMember / CreateInvitation rejected | Yes |
| Deletable | No — DeleteWorkspace rejected | Yes, by its owner |
Every user owns exactly one PERSONAL workspace, created lazily
the first time they call ListWorkspaces, so a user never has a
"no workspace" state. A personal workspace is closed:
single-member and undeletable, enforced structurally by the
WorkspaceType enum rather than by counting members. Team
workspaces are user-created for collaboration — a family or a whole company.
curl -X POST http://localhost:8080/workspace.v1.WorkspaceService/ListWorkspaces \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"acting_user_id":"alice"}'# On alice's first call this auto-provisions and returns her PERSONAL workspace.Roles
Every team membership carries a Role, ordered
owner ⊃ admin ⊃ member ⊃ guest. Each grade includes the
powers of every grade below it.
| Role | Can do |
|---|---|
ROLE_OWNER | Everything an admin can, plus delete the workspace and transfer ownership. Set at creation; never granted afterward. |
ROLE_ADMIN | Add / remove members, change member roles (not owner), create / revoke invitations, update the workspace. |
ROLE_MEMBER | Belong to the workspace; gains viewer on resources in it through inheritance. |
ROLE_GUEST | The weakest grade — present in the workspace for narrowly scoped, explicitly shared access. |
Gotcha — owner is not grantable. AddMember, UpdateMemberRole, and
CreateInvitation reject ROLE_OWNER with
InvalidArgument. Ownership is set when the workspace is
created and transferred explicitly — there is never a second owner. The
owner's role cannot be changed and the owner cannot be removed.
Membership maps to tuples
Each membership is mirrored as a
workspace:<id>#<role>@user:<id> tuple, so a
Check against the workspace namespace honours it
immediately and resource inheritance treats personal and team workspaces
identically.
workspace:acme#owner@user:ceoworkspace:acme#admin@user:eng-leadworkspace:acme#member@user:aliceAdding eng-lead as an admin writes the second tuple:
curl -X POST http://localhost:8080/workspace.v1.WorkspaceService/AddMember \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"workspace_id":"acme","user_id":"eng-lead","role":"ROLE_ADMIN","acting_user_id":"ceo"}'Invitations
Invitations bring a person into a team workspace by email when you do not yet know their user id.
CreateInvitation(workspace_id, email, role)returns a one-time plaintexttoken, echoed only on creation, for out-of-band delivery (your product emails it). LikeAddMember, it rejectsROLE_OWNER.- The token has a 7-day TTL. After it expires the invite moves to
EXPIREDand can no longer be accepted. AcceptInvitation(token, acting_user_id)consumes the token — it is single-use — and addsacting_user_idto the workspace at the invited role. If they are already a member, the invite is consumed and the existing role is kept.ListInvitationsreturns pending invites but never echoes tokens.RevokeInvitationcancels a pending one.
curl -X POST http://localhost:8080/workspace.v1.WorkspaceService/CreateInvitation \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"workspace_id":"acme","email":"new@example.com","role":"ROLE_MEMBER","acting_user_id":"ceo"}'# => {"invitation":{ ... ,"token":"<one-time-token>"}} ← only place the token appearsGroups — a separate, nestable primitive
A group is a reusable set of subjects — an email distribution list, a chat
group, an on-call rotation, a B2C user's "family" list. It is deliberately
not a workspace (see
ADR-0003).
Groups map to the group namespace, relation member,
whose rewrite is this — just stored tuples.
Because a tuple's subject can be a userset, granting a group access to
anything is a single tuple, and groups nest (a
GroupMember is a oneof of a user_id or
a nested group_id):
group:all-eng#member@group:backend#member # all-eng contains backend (nesting)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#member # a B2C user's reusable "family" list
A group is reusable: the same group is referenced by id from
any number of workspace or resource tuples. It may be
project-level (empty workspace_id — a B2C
user's standalone "family" list) or scoped to a workspace
(a company's internal groups). Scoping is an organisational convenience for
listing and ownership, not a reuse boundary — a workspace-scoped group can
still be granted access on any resource in the same project.
Workspace or group?
| Workspace | Group | |
|---|---|---|
| What it is | A tenancy / ownership boundary people belong to | A named set of subjects you grant access with |
| Owner | Exactly one owner | No role hierarchy — flat membership |
| Roles | owner ⊃ admin ⊃ member ⊃ guest | Just member |
| Nests? | No | Yes — groups contain groups |
| Reuse | Owns its resources; not referenced elsewhere | Referenced from many workspaces & resources |
| Resource inheritance | Resources inherit from their parent workspace | None — a group is only ever a subject |
| Use it when | It owns resources and is a billing / tenancy unit | It is a set of people you reuse across grants |
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.
Where to next
- Roles & RBAC — modeling membership in depth.
- Groups & Nesting — building reusable, nested sets.
- Projects & Multi-Tenancy — how everything is sharded.