Designing your resource tree
Choose access boundaries, share through groups, and understand the read, write, and storage tradeoffs of SHRBAC.
Your application can have one document list, one inspections screen, or one work queue for everyone. A company administrator, a department manager, and an external reviewer use the same endpoint; their grants determine which records it returns.
SqlOS makes that possible by treating access as part of your data model. Put resources beneath the place where their access belongs, then grant roles at that boundary. You do not need a different authorization predicate for every persona.

All three example roles include document.read. The team has no access to Workspace B's documents from its Workspace A grant. The guest's grant on Document D does not authorize Workspace B or Document C. Grants may belong to users, groups, service accounts, or agents; being named an administrator or owning a workspace does not create a grant automatically.
For a document editor, create a personal workspace when someone signs up, grant them a role containing document.read on it, and place their documents beneath it. A new document inherits that access without another grant. When the product adds teams, grant the appropriate role to a group on a shared workspace. Share one document with an outside reviewer by granting a narrower role on that document.
The same design applies beyond documents:
| Application | Example resource tree | A useful grant boundary |
|---|---|---|
| Inspections | Organization → team → inspection → item | Team for supervisors; inspection for an assigned reviewer |
| Case management | Agency → department → case → record | Department for staff; case for outside collaborators |
| Retail | Company → region → store → inventory item | Region for managers; store for clerks |
| Document editing | Application → workspace → document | Workspace for members; document for an individual share |
A resource has one parent. Choose the parent for permission inheritance, not simply because two records have a foreign key relationship. Other relationships can remain ordinary application relationships. If access must arrive through two independent branches, model the additional access with grants or groups; the resource tree itself is not a multi-parent graph.
One workspace grant can cover its current and future documents. It is easier to administer than creating a grant for every document.
“Highest” is relative to the access you mean to give. A department reviewer should receive a department grant, not a root grant. A collaborator who may read one document should receive a document grant, not a workspace grant. Do not broaden access just to reduce the number of grants.
Roles supply permissions; placement supplies scope. A workspace role may contain document.read without document.edit. A permission restricted to the document resource type authorizes documents beneath the workspace, not every other kind of descendant. Access flows down through active resources, never up or sideways. An inactive resource interrupts inheritance from above; grants below it can still authorize active descendants. See resource hierarchy and permissions.
Use a group when several subjects should share the same access. Grant to the group once and manage its membership. This avoids duplicating the grant for every member, but a group with grants on 100 independent branches still represents 100 branches.
var page = await db.Documents
.OrderBy(d => d.Name)
.ToPageAsync(subjectId, "document.read", pageSize: 20, cursor, ct);The same query serves the three callers in the diagram. It needs no OwnerId == subjectId or TenantId == tenantId condition to discover their authorized documents. Add a workspace or tenant condition when the screen itself is scoped there; it is a product filter, not a replacement for authorization. Authentication still happens at the request boundary, and writes still need the appropriate permission check.
Index the order the screen pages by, HasIndex(d => new { d.Name, d.Id }); SqlOS keeps a copy of every index under every node of the resource tree. Paginating authorized lists covers the rule, the cursor, and what a page costs.
ToPageAsync resolves the caller's principals (the subject plus its active groups) and the permission each time it runs. It first reads the screen's query in order and checks each row, which fills the page for a caller who can see most of it. For a caller who can see little, it reads its copy of the order's index under each node the caller is granted and merges them: one index seek per node. So a page costs about a page whether the caller sees everything, one branch, or three rows of a million.
Grants on nodes keep that cheap. The second read seeks once per granted node (grants under another of the caller's grants add nothing). A workspace grant is one seek; a group with grants on 100 independent branches is 100 seeks; ten thousand single-document grants are ten thousand seeks. Grant where the access belongs, and use groups and higher boundaries rather than many individual shares when that is the access you mean.
SqlOS has no authorized count or join, because those cost every row the caller can see. Show a page and its HasNextPage instead of a total, and check one resource with CheckAccessAsync. Test broad access, sparse individual shares, later cursors, and empty results using your application's real Where conditions and orders.
Your application keeps ordinary tables and relationships, connected to FGA through ResourceId. Entities implementing ISqlOSResourceEntity describe their resource and parent; SqlOSDbContext<TContext> synchronizes those resource rows during saves. Grants remain explicit.
SqlOS owns its resource, principal, group, role, permission, and grant tables and the authorization functions. For every index of a protected table, and for its primary key, it also keeps one table in its own schema with a copy of that order under every node, rewritten in the same transaction as the saves that change it. It adds nothing to your tables: no scope columns, triggers, or indexes.
Changing a resource's parent changes which ancestor grants can apply on subsequent queries. Keep hierarchy changes deliberate because they change access, and check the appropriate permissions before making them. Pages and checks resolve the caller's principals and groups each time they run, so a grant or membership change applies from the next page or check.