The pattern
Model anorganization (or tenant) type and make every other resource belong to it:
organizationhas members, admins, and owners.workspace,project, anddocumenttypes each declare aparentrelation pointing at anorganization.- Relations like
can_viewandcan_edittraverse the parent edge so organization membership grants the right access automatically.
- Hard tenant isolation by default. A user only has tuples for the organizations they belong to, so list-objects naturally returns nothing cross-tenant.
- Cross-tenant sharing when you want it. A document can also have individually granted users from other organizations — same model, no special case.
- A single store for all tenants. Operationally cheaper than per-tenant databases; the tuple count grows linearly with usage rather than fan-out.
Per-tenant custom roles
Most SaaS products eventually need tenant-defined roles — “Acme wants abilling-manager role; Globex wants a compliance-reviewer role” — without redeploying the model. The pattern is to make role a first-class type whose assignee relation can be a user, another role, or a group, and then bind those roles to organization-level permissions:
Each tenant creates its own role:acme-billing-manager, role:acme-document-management, etc., and binds them to the predefined organization-level relations (billing_manager, document_manager, admin). Users or groups become assignees of those roles, and document-level permissions inherit through the organization. The model itself stays static; tenants get full flexibility on which roles exist and who is in them.
The full working example — including tuples, group nesting (group:acme-data-engineering#member ∈ group:engineering#member), and list_users assertions — is in the multitenant-rbac sample store. You can run it locally with the FGA CLI to see the checks pass.