Last updated 2026-06-29
Learning Platform
This page models a learning platform that serves both B2C learners who buy a
single course and companies who buy seats that grant a whole roster access.
One engine, one set of namespaces: a course, cohort, or lesson is a
resource; a company is a TEAM workspace; a
class or cohort is a group; instructors are
editors and learners are viewers. You will see the
exact tuples and Check answers for enrolling, instructor
grading access, and company-admin oversight.
The model
- Course / lesson →
resource. A lesson isparented to its course, so course-level grants cascade to every lesson. - Instructor →
editor; learner →viewer.resource.viewerunionscomputed(editor), so every instructor is automatically a viewer too. - Company →
workspace(TEAM); class / cohort →group. Seat-holders are workspace members; a cohort is a reusable membership set granted on a course in one tuple.
B2C · a solo learner enrolls
An individual buyer needs no organization. On first
ListWorkspaces the service auto-provisions their
PERSONAL workspace; it is closed and never used to share course
content. Enrollment is a single direct grant —
resource:course#viewer@user — the this leaf of the
viewer rule, with no parent workspace.
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":"course-go","relation":"viewer","subject":{"user_id":"jo"}}} ] }'resource:course-go#viewer@user:jo # jo bought it personally
The gate the product checks before streaming a lesson — lessons inherit
course access via parent:
resource:lesson-1#parent@resource:course-go # lesson belongs to the coursecurl -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":"lesson-1","relation":"viewer","subject_user_id":"jo"}'{"allowed": true}
The viewer rule's tupleToUserset(parent, member)
does not match here (the course's parent is another resource, not a
workspace), but computed(editor) and the course's own
viewer grant flow through course → lesson, so jo is allowed.
B2B · a company buys seats
A company is a TEAM workspace. The platform backend creates it
and adds each seat-holder as a member —
acting_user_id is required on every management call.
# create the company workspacecurl -X POST http://localhost:8080/workspace.v1.WorkspaceService/CreateWorkspace \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"display_name":"BigCo","slug":"bigco","acting_user_id":"bigco-admin"}'
# fill a seat (ROLE_OWNER is rejected here; owner is fixed at creation)curl -X POST http://localhost:8080/workspace.v1.WorkspaceService/AddMember \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" -H "Content-Type: application/json" \ -d '{"workspace_id":"bigco","user_id":"employee-1","role":"ROLE_MEMBER","acting_user_id":"bigco-admin"}'
Grant the catalogue to the company by parenting each course to
the workspace. Now every seat-holder is a viewer by
inheritance — adding a seat is one AddMember call and
it grants the whole catalogue at once.
workspace:bigco#owner@user:bigco-adminworkspace:bigco#member@user:employee-1workspace:bigco#member@user:employee-2resource:course-go#parent@workspace:bigco # bigco's seats can view 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":"course-go","relation":"viewer","subject_user_id":"employee-1"}'{"allowed": true}
The viewer rule's tupleToUserset(parent, member)
walks the course's parent to workspace:bigco,
evaluates member, and allows. The same course object thus
serves both a seat-based company and a one-off B2C buyer: inheritance for the
roster, a direct grant for the individual.
Cohorts and instructor grading access
A class or cohort is a group — reusable across the courses it
touches. The instructor is granted editor directly (which makes
them a viewer too); the cohort group is granted
viewer so the whole roster sees the material.
# the cohort, and a member of itgroup:cohort-2026#member@user:learner-pat# instructor edits (grades) the course; cohort views itresource:course-go#editor@user:instructor-samresource:course-go#viewer@group:cohort-2026#member
Grading writes need editor; the product checks it before
accepting a grade. Because editor unions
computed(owner) and the direct this grant,
instructor-sam is 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":"course-go","relation":"editor","subject_user_id":"instructor-sam"}'{"allowed": true}
A cohort learner, by contrast, returns
{"allowed": false} for editor but
{"allowed": true} for viewer — they can watch,
not grade.
Company-admin oversight
Company admins need to see what their seats can access without an explicit
per-course grant. Because resource.editor unions
tupleToUserset(parent, admin), any
workspace:bigco#admin can edit every course parented to the
company — oversight is inherited from the workspace role, not granted course
by course.
workspace:bigco#admin@user:program-manager # an L&D admincurl -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":"course-go","relation":"editor","subject_user_id":"program-manager"}'{"allowed": true}
To audit a course's full reach, call
Expand(resource, course-go, viewer) — the tree unions the direct
grants, the editor subtree (instructors), the cohort group's
member userset, and the parent workspace's
member set. See
Check & Expand.
One model, both audiences
The decision is always
Check(resource, <course>, viewer, <user>) — the
product never branches on B2C versus B2B. To isolate one large customer's
data, give them their own project_id shard (see
Projects & Multi-Tenancy);
otherwise one B2C product is one project with many personal workspaces. For
personal-vs-team semantics, see
Workspaces & Groups.