Last updated 2026-06-29
Workplace Collaboration Tool
This page models a Slack-plus-Linear-plus-email-plus-HR-plus-incident.io
workplace product entirely as relation tuples — no per-feature permission
tables. You will see the exact tuples written as a company onboards, a team
forms, an issue is filed, a doc is shared with a group, and an on-call
rotation is granted incident access — and the Check answers
each one produces.
The model
Everything maps onto three namespaces and their userset-rewrite rules from the Authorization Model:
- Company →
workspace(TEAM). Employees are members at a role;owner ⊃ admin ⊃ member ⊃ guest. - Departments, teams, on-call rotations →
group. Groups nest, soall-engcan containbackendandfrontend. - A Linear issue, a channel, an HR record, an incident →
resource. Each isparented to the company workspace (so it inherits company access) and can additionally carry direct shares and group grants.
The two rewrite rules that do all the inheritance work:
resource.viewer unions this, the resource's own
editors, and tupleToUserset(parent, member);
resource.editor unions this, the resource's
owner, and tupleToUserset(parent, admin). That is
how "any company admin can edit anything in the company" is a single rule,
not a join rewritten per feature.
1 · A company onboards
The product backend calls CreateWorkspace on behalf of the
founder. acting_user_id is required — it is the user the action
is authorized as (the end user is data here, never the caller). The owner is
set at creation and mirrored as a workspace:acme#owner tuple.
curl -X POST http://localhost:8080/workspace.v1.WorkspaceService/CreateWorkspace \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"display_name":"Acme","slug":"acme","acting_user_id":"ceo"}'
Then employees are added with AddMember. Note
AddMember rejects ROLE_OWNER — ownership is fixed at
creation — so new staff arrive as admin, member, or guest.
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"}'The resulting company-membership tuples:
workspace:acme#owner@user:ceoworkspace:acme#admin@user:eng-leadworkspace:acme#member@user:aliceworkspace:acme#member@user:bob2 · A team is formed
Departments and teams are groups scoped to the workspace
(workspace_id set). Groups nest, so the backend team is a member
of all-engineering. Create the group, then add members — a member can be a
user or another group.
# create the team as a workspace-scoped groupcurl -X POST http://localhost:8080/workspace.v1.GroupService/CreateGroup \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"display_name":"Backend","slug":"backend","workspace_id":"acme","acting_user_id":"eng-lead"}'
# add a user to the teamcurl -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":"eng-lead"}'
# nest the backend team inside all-engineeringcurl -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":"eng-lead"}'The tuples these write — note the nested-group subject is a userset:
group:backend#member@user:alicegroup:backend#member@user:bobgroup:all-eng#member@group:backend#member # backend nests into all-eng
Because group.member = this and subjects may themselves be
group:<id>#member usersets,
Check(group, all-eng, member, alice) walks
backend transitively and returns
allowed. Evaluation is cycle-safe.
3 · An issue is created
A Linear-style issue is a resource parented to the company
workspace, with the filer as its owner. That is two tuples —
and every Acme member can already view it and every Acme admin can already
edit it, by inheritance, with zero per-issue tuples for those users.
curl -X POST http://localhost:8080/workspace.v1.AuthzService/WriteRelationTuples \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{ "updates": [ {"op":"OP_INSERT","tuple":{"namespace":"resource","object_id":"issue-42","relation":"parent","subject":{"set":{"namespace":"workspace","object_id":"acme","relation":""}}}}, {"op":"OP_INSERT","tuple":{"namespace":"resource","object_id":"issue-42","relation":"owner","subject":{"user_id":"alice"}}} ] }'resource:issue-42#parent@workspace:acme # the issue lives in acmeresource:issue-42#owner@user:alice # alice filed it
Now Check(resource, issue-42, viewer, bob) for an ordinary Acme
member: the viewer rule's
tupleToUserset(parent, member) walks the issue's
parent to workspace:acme, evaluates
member there, and returns allowed.
curl -X POST http://localhost:8080/workspace.v1.AuthzService/Check \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"namespace":"resource","object_id":"issue-42","relation":"viewer","subject_user_id":"bob"}'{"allowed": true}
A guest with no path to the company workspace, by contrast, gets
{"allowed": false} for the same call.
4 · A doc is shared with a group
An HR record, a channel, or a design doc is shared with a whole team in a
single tuple whose subject is the group's
member userset. Add or remove a teammate once and access
propagates to every resource that group is granted on.
curl -X POST http://localhost:8080/workspace.v1.AuthzService/WriteRelationTuples \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{ "updates": [ {"op":"OP_INSERT","tuple":{"namespace":"resource","object_id":"doc-roadmap","relation":"editor","subject":{"set":{"namespace":"group","object_id":"all-eng","relation":"member"}}}} ] }'resource:doc-roadmap#editor@group:all-eng#member Check(resource, doc-roadmap, editor, alice) resolves
group:all-eng#member, which nests backend, which
contains alice — allowed. Because
viewer unions computed(editor), alice is also a
viewer with no second tuple.
5 · An on-call rotation is granted incident access
An incident is a resource; the on-call rotation is a
group. Grant the rotation editor on the incident in
one tuple. Whoever is on-call this week — set by group membership — can edit
it, with no change to the incident's tuples.
group:sre-oncall#member@user:bob # bob is on the rotationresource:incident-7#parent@workspace:acme # incident lives in acmeresource:incident-7#editor@group:sre-oncall#member # on-call can edit itcurl -X POST http://localhost:8080/workspace.v1.AuthzService/Check \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"namespace":"resource","object_id":"incident-7","relation":"editor","subject_user_id":"bob"}'{"allowed": true}
Rotate the on-call by deleting bob's group:sre-oncall#member
tuple and inserting the next responder's — the incident grant never changes.
Inspecting who has access
Expand(resource, doc-roadmap, viewer) returns the effective
userset tree — a union with this, the editor
subtree (carrying group:all-eng#member), and the
tupleToUserset(parent, member) subtree resolving to
workspace:acme#member. Use it to render a "shared with" panel
or to debug why a Check resolved the way it did. See
Check & Expand.
Why this scales
HR records, incidents, issues, channels, and docs are all
resources parented to the company workspace, and they all
inherit company access through the same two rewrite rules. New features add
no permission tables and no new Check code — only new tuples.
For the personal-vs-team boundary and standalone groups, see
Workspaces & Groups.