Last updated 2026-06-29
Personal-Assistant App
This page models a consumer task app where end users share individual tasks and lists with specific people — a partner, a parent, the whole family — with no enclosing organization. Each user lives in their auto-provisioned personal workspace, which is closed; sharing happens at the resource level, not by adding members to that workspace. You will see the tuples for creating a task, sharing it with a person, sharing it with a family group, and revoking access.
The personal workspace is closed
There is no "create org" step. On the user's first
ListWorkspaces call the service auto-provisions their single
PERSONAL workspace (see
ADR-0002).
It is undeletable and admits exactly one member — the owner. You
cannot add a friend to it, and the API rejects attempts to.
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"}'{ "workspaces": [ {"id":"ws_personal_alice","slug":"alice","display_name":"Alice","type":"WORKSPACE_TYPE_PERSONAL","owner_user_id":"alice"} ]} Key point: the personal workspace is a private home, not a
sharing surface. Collaboration is expressed by granting another user a
relation on a specific resource — never by widening workspace
membership.
1 · Create a task
A task or list is a resource the user owns. Creating it is a
single tuple: resource:<task>#owner@user. The owner gets
edit and view for free, because editor unions
computed(owner) and viewer unions
computed(editor).
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":"task-buy-milk","relation":"owner","subject":{"user_id":"alice"}}} ] }'resource:task-buy-milk#owner@user:alice
Because the task has no parent tuple, there is no inheritance —
access is exactly what is granted directly on it. This is the deliberate
contrast with the workplace and learning examples, where a
parent workspace drives access.
2 · Share with a person
Sharing with one individual is one direct tuple — the this leaf
of the editor (or viewer) rule. Give dad edit
rights so he can tick the task off:
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":"task-buy-milk","relation":"editor","subject":{"user_id":"dad"}}} ] }'resource:task-buy-milk#editor@user:dadcurl -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":"task-buy-milk","relation":"editor","subject_user_id":"dad"}'{"allowed": true}
A friend who was never shared the task returns
{"allowed": false} — there is no organization-wide default,
so unshared means no access.
3 · Share with the family group
For people the user shares with repeatedly, a standalone, project-level
group (empty workspace_id) is reusable across every
task. Create it once, then grant it viewer on a task in a single
tuple whose subject is the group:family#member userset.
# create the standalone family group (no owning workspace)curl -X POST http://localhost:8080/workspace.v1.GroupService/CreateGroup \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"display_name":"Family","slug":"family","acting_user_id":"alice"}'
# add a relative to the groupcurl -X POST http://localhost:8080/workspace.v1.GroupService/AddGroupMember \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"group_id":"family","member":{"user_id":"gran"},"acting_user_id":"alice"}'
# grant the whole family viewer on the task in one tuplecurl -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":"task-buy-milk","relation":"viewer","subject":{"set":{"namespace":"group","object_id":"family","relation":"member"}}}} ] }'group:family#member@user:granresource:task-buy-milk#viewer@group:family#member Check(resource, task-buy-milk, viewer, gran) resolves the
group:family#member userset transitively to gran and returns
allowed. Add or remove a relative once — by editing group
membership — and it propagates to every task the family group is granted on.
4 · Revoke
Revoking is the inverse operation: delete the tuple. Removing dad's direct
grant is an OP_DELETE of the exact tuple; removing a relative
from everything at once is an OP_DELETE of their
group:family#member tuple.
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_DELETE","tuple":{"namespace":"resource","object_id":"task-buy-milk","relation":"editor","subject":{"user_id":"dad"}}} ] }'
An immediate re-Check for dad now returns
{"allowed": false}. Note that removing the group grant
(resource:task-buy-milk#viewer@group:family#member) revokes the
whole family from that task, while removing
group:family#member@user:gran revokes gran from
every task shared with the family — choose the tuple that matches
the scope you mean.
The whole product, in tuples
A closed personal workspace per user, tasks as directly-shared resources, and
a reusable family group — no per-app collaborator table, no membership in
anyone's private workspace. The same engine that backs a company
collaboration tool backs a single-user task sharer; only the tuples differ.
See the
Authorization Model
for how the this leaf and userset subjects combine, and
Sharing Resources for the general
pattern.