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, so all-eng can contain backend and frontend.
  • A Linear issue, a channel, an HR record, an incident → resource. Each is parented 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.

onboard the company
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.

add an engineering lead as admin
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:

company membership tuples
workspace:acme#owner@user:ceo
workspace:acme#admin@user:eng-lead
workspace:acme#member@user:alice
workspace:acme#member@user:bob

2 · 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.

form a nested team
# create the team as a workspace-scoped group
curl -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 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":"eng-lead"}'
# nest the backend team inside all-engineering
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":"eng-lead"}'

The tuples these write — note the nested-group subject is a userset:

team membership tuples
group:backend#member@user:alice
group:backend#member@user:bob
group: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.

file the issue
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"}}}
]
}'
issue tuples
resource:issue-42#parent@workspace:acme # the issue lives in acme
resource: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.

can bob view the issue?
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.

share the roadmap with all of engineering
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"}}}}
]
}'
group grant tuple
resource:doc-roadmap#editor@group:all-eng#member

Check(resource, doc-roadmap, editor, alice) resolves group:all-eng#member, which nests backend, which contains aliceallowed. 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.

incident + on-call tuples
group:sre-oncall#member@user:bob # bob is on the rotation
resource:incident-7#parent@workspace:acme # incident lives in acme
resource:incident-7#editor@group:sre-oncall#member # on-call can edit it
can bob edit the incident?
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":"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.