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.

auto-provision alice's personal workspace
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).

alice creates a task
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"}}}
]
}'
task tuple
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:

share the task with dad as 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":"editor","subject":{"user_id":"dad"}}}
]
}'
direct share tuple
resource:task-buy-milk#editor@user:dad
can dad edit the task?
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":"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.

reusable family group + one grant
# 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 group
curl -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 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_INSERT","tuple":{"namespace":"resource","object_id":"task-buy-milk","relation":"viewer","subject":{"set":{"namespace":"group","object_id":"family","relation":"member"}}}}
]
}'
group membership + group grant
group:family#member@user:gran
resource: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.

revoke dad's access
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.