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 AppDeep InheritanceDeep 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

Deep Inheritance

Model authorization for an application where users navigate through nested resources and a single detail page renders many permission-gated 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
  • Putting it all together
  • Next steps

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.createRoleAssignment({
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. Use listResourcesForMembership with the appropriate parent_resource filter to scope results to the current view.

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);
});

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.

curl "https://api.workos.com/authorization/organization_memberships/om_01HXYZ/resources/app/app_01H/permissions" \
-H "Authorization: Bearer sk_example_123456789"
app.get('/apps/:appId', async (req, res) => {
const { organizationMembershipId } = req.user;
const { appId } = req.params;
const { data: permissions } =
await workos.authorization.listEffectivePermissions({
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.

Putting it all together

The complete flow for a deeply nested app:

1. Model → Three resource types: workspace → project → app
2. Privileges → Roles at each level; parent roles include child permissions
3. Resources → Register each level with the appropriate parent reference
4. Navigation → listResourcesForMembership(parent filter) at each level
5. Detail page → listEffectivePermissions(leaf resource) → render gated components
6. Endpoints → check (one + one), listEffectivePermissions (many + one), listResourcesForMembership (one + many)

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.

Next steps

Other “Model your app” guides:

  • Basic App – Single resource type with list and detail views. See Basic App.
  • Protecting API Endpoints – Using check for every CRUD operation. See Protecting API Endpoints.
  • High-Cardinality Data – Files, messages, and rows that inherit access from an FGA-managed parent. See High-Cardinality Data.
  • Share Button – A sharing dialog backed by resource discovery and role assignments. See Share Button.

Related reference:

  • Resource types – Modeling hierarchies
  • Roles and permissions – How inheritance works
  • Access checks – Endpoint reference
  • Resource discovery – Listing accessible resources and members
Protecting API EndpointsUse 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.