WorkOS Docs Homepage
FGA
API referenceDashboardSign In
OverviewOverviewQuick StartQuick StartCore ConceptsResource TypesResource TypesResourcesResourcesRoles and PermissionsRoles and PermissionsAssignmentsAssignmentsHigh-Cardinality EntitiesHigh-Cardinality EntitiesAccess ControlAccess ChecksAccess ChecksResource DiscoveryResource DiscoveryIntegrationsAuthKit IntegrationAuthKit IntegrationStandalone IntegrationStandalone IntegrationIdP Role AssignmentIdP Role AssignmentModel Your AppBasic AppBasic AppMulti-level InheritanceMulti-level InheritanceProtecting API EndpointsProtecting API EndpointsHigh-Cardinality DataHigh-Cardinality DataShare ButtonShare ButtonMigration GuidesMigrate from OpenFGAMigrate from OpenFGAMigrate from SpiceDBMigrate from SpiceDBMigrate from Oso CloudMigrate from Oso Cloud
API Reference
API Reference
Events
Events
Integrations
Integrations
Migrate to WorkOS
Migrate to WorkOS
SDKs
SDKs

Multi-level Inheritance

Model authorization for nested resources and permission-gated UI components.

On this page

  • What you’ll build
  • 1. Model the hierarchy
  • 2. Define roles and permissions
  • 3. Register resources as they are created
  • 4. Navigate the hierarchy
    • List workspaces
    • List projects inside a workspace
  • 5. Power the detail page with effective permissions
  • 6. When to use each access check endpoint

What you’ll build

This guide extends the Basic App to a multi-level hierarchy where users navigate down through nested resources. At each level, the list shows only the resources the user can access. At the leaf, a detail page renders many UI components, each gated on a different permission.

The example is a deployment platform with three resource types: workspaces contain projects, and projects contain apps. By the end, you’ll have:

  • A three-level resource type hierarchy
  • Navigation views that list workspaces, then projects within a workspace, then apps within a project
  • An app detail page that fetches the user’s effective permissions in a single call and renders the appropriate components
  • An understanding of when to use listEffectivePermissions instead of repeated check calls
organization (implicit root)
└─ workspace
└─ project
└─ app

1. Model the hierarchy

Create three resource types in the WorkOS Dashboard under Authorization > Resource Types. Each one points to its parent.

NameSlugParent
WorkspaceworkspaceOrganization
ProjectprojectWorkspace
AppappProject

Permissions assigned higher in the hierarchy flow down. A user with a workspace-scoped role automatically has the corresponding access on every project and app inside that workspace. See Resource types for hierarchy constraints and validation rules.

2. Define roles and permissions

Define permissions for each resource type, then create roles that bundle them. Roles can include permissions for child types, which is what enables inheritance across the hierarchy.

PermissionResource typeDescription
workspace:viewworkspaceView a workspace
workspace:manageworkspaceEdit workspace settings
project:viewprojectView a project
project:editprojectEdit a project
project:create_appprojectCreate apps inside a project
app:viewappView an app
app:deployappDeploy an app
app:configureappEdit app settings
app:view_logsappRead deployment logs
app:deleteappDelete an app

Bundle these into roles scoped to each resource type. A role scoped to workspace can include permissions on project and app because those are descendant types.

RoleScoped toPermissions
workspace-adminworkspaceworkspace:view, workspace:manage, project:view, project:edit, project:create_app, app:view, app:deploy, app:configure, app:view_logs, app:delete
workspace-memberworkspaceworkspace:view, project:view, app:view
project-editorprojectproject:view, project:edit, project:create_app, app:view, app:deploy, app:configure, app:view_logs
app-deployerappapp:view, app:deploy, app:view_logs

A workspace-admin assignment on a single workspace grants full control of every project and app in it without any per-resource assignment. A project-editor assignment grants access to one project and its apps. An app-deployer assignment grants access to one app only. See Roles and permissions for more on inheritance.

3. Register resources as they are created

Register the corresponding FGA resource each time a workspace, project, or app is created in the database. Use the database ID as the external_id and reference the parent by its external ID and type.

import { WorkOS } from '@workos-inc/node';
const workos = new WorkOS(process.env.WORKOS_API_KEY);
app.post('/workspaces', async (req, res) => {
const { organizationId, organizationMembershipId } = req.user;
const { name } = req.body;
const workspace = await db.workspaces.create({
data: { name, organizationId },
});
await workos.authorization.createResource({
organizationId,
resourceTypeSlug: 'workspace',
externalId: workspace.id,
name: workspace.name,
});
await workos.authorization.assignRole({
organizationMembershipId,
roleSlug: 'workspace-admin',
resourceTypeSlug: 'workspace',
resourceExternalId: workspace.id,
});
return res.json(workspace);
});
app.post('/workspaces/:workspaceId/projects', async (req, res) => {
const { organizationId } = req.user;
const { workspaceId } = req.params;
const { name } = req.body;
const project = await db.projects.create({
data: { name, workspaceId, organizationId },
});
await workos.authorization.createResource({
organizationId,
resourceTypeSlug: 'project',
externalId: project.id,
name: project.name,
parentResourceTypeSlug: 'workspace',
parentResourceExternalId: workspaceId,
});
return res.json(project);
});

Apps follow the same pattern with parentResourceTypeSlug: 'project'. Note that the workspace creator gets workspace-admin, which already includes every project and app permission. There’s no need to create per-project or per-app assignments for the creator – inheritance handles it.

4. Navigate the hierarchy

At each level, the UI lists the resources the user can access.

List workspaces

app.get('/workspaces', async (req, res) => {
const { organizationMembershipId } = req.user;
const { data } = await workos.authorization.listResourcesForMembership({
organizationMembershipId,
permissionSlug: 'workspace:view',
resourceTypeSlug: 'workspace',
});
const workspaces = await db.workspaces.findMany({
where: { id: { in: data.map((r) => r.externalId) } },
});
return res.json(workspaces);
});

Use listResourcesForMembership with the appropriate parent_resource filter to scope results to the current view.

List projects inside a workspace

app.get('/workspaces/:workspaceId/projects', async (req, res) => {
const { organizationMembershipId } = req.user;
const { workspaceId } = req.params;
const { data } = await workos.authorization.listResourcesForMembership({
organizationMembershipId,
permissionSlug: 'project:view',
resourceTypeSlug: 'project',
parentResourceTypeSlug: 'workspace',
parentResourceExternalId: workspaceId,
});
const projects = await db.projects.findMany({
where: { id: { in: data.map((r) => r.externalId) } },
});
return res.json(projects);
});

A user with workspace-admin on the workspace appears in this list for every project, because project:view is included in the role through inheritance. A user with project-editor on a single project appears only for that project. A user with neither role sees nothing.

The app list follows the same pattern with permission_slug=app:view and parent_resource_type_slug=project.

5. Power the detail page with effective permissions

The app detail page renders multiple components, each gated on a different permission. Rather than calling check once per component, use listEffectivePermissions to fetch every permission the user has on the resource in a single call. Inherited permissions from workspace and project roles are included automatically.

app.get('/apps/:appId', async (req, res) => {
const { organizationMembershipId } = req.user;
const { appId } = req.params;
const { data: permissions } =
await workos.authorization.listEffectivePermissionsByExternalId({
organizationMembershipId,
resourceTypeSlug: 'app',
externalId: appId,
});
const slugs = new Set(permissions.map((p) => p.slug));
if (!slugs.has('app:view')) {
return res.status(404).json({ error: 'Not found' });
}
const app = await db.apps.findUnique({ where: { id: appId } });
return res.json({
app,
permissions: {
canDeploy: slugs.has('app:deploy'),
canConfigure: slugs.has('app:configure'),
canViewLogs: slugs.has('app:view_logs'),
canDelete: slugs.has('app:delete'),
},
});
});

The returned permission list reflects the user’s full access on this resource, including everything inherited from roles on parent resources. A workspace admin sees every app:* permission here, even though they don’t have any direct assignment on the app itself.

The React component renders one section per permission:

function AppDetail() {
const { appId } = useParams();
const { data, isLoading } = useQuery(`/apps/${appId}`);
if (isLoading) return <Spinner />;
if (!data) return <NotFound />;
const { app, permissions } = data;
return (
<article>
<h1>{app.name}</h1>
<Overview app={app} />
{permissions.canViewLogs && <LogsPanel appId={app.id} />}
{permissions.canDeploy && <DeployButton appId={app.id} />}
{permissions.canConfigure && <SettingsForm app={app} />}
{permissions.canDelete && <DangerZone appId={app.id} />}
</article>
);
}

Every component above the fold is rendered based on the same permission set, fetched once. There are no waterfall requests as the page hydrates, and adding a new permission-gated component is a one-line change.

6. When to use each access check endpoint

FGA exposes three endpoints for answering authorization questions. Pick the one that matches the shape of the question.

EndpointUse when
checkA single permission on a single resource. Best for action handlers and route gates.
listEffectivePermissionsMany permissions on a single resource. Best for detail pages that render multiple permission-gated components.
listResourcesForMembershipA single permission across many resources. Best for list views, navigation, and pickers.

The pattern from the Basic App used check for a single permission gate. As soon as the detail page needs more than two or three permissions, prefer listEffectivePermissions. It returns the full permission set in one call instead of fanning out to multiple check requests.

The same applies in the opposite direction. When the question is “which resources can this user access,” use listResourcesForMembership rather than calling check once per candidate resource.

Inheritance is what makes this scale. A single workspace-admin assignment grants access to every project and app under that workspace. The list endpoints surface inherited access automatically, and the detail page sees the full inherited permission set in one fetch.

Protecting API Endpoints Use the check endpoint to protect every CRUD operation in your REST API
Up next
© WorkOS, Inc.
FeaturesAuthKitSingle Sign-OnDirectory SyncAdmin PortalFine-Grained Authorization
DevelopersDocumentationChangelogAPI Status
ResourcesBlogPodcastPricingSecuritySupport
CompanyAboutCustomersCareersLegalPrivacy
© WorkOS, Inc.