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

Basic App

Model authorization for a project management app with list and detail pages.

On this page

  • What you’ll build
  • 1. Model the hierarchy
  • 2. Define roles and permissions
  • 3. Register projects as resources
  • 4. Grant access to other users
  • 5. Power the list view
    • Pagination
  • 6. Power the detail view
  • 7. Handle empty and forbidden states
  • Putting it all together
  • Next steps

What you’ll build

This guide shows how to use FGA in the two most common UI patterns in a B2B application: a list view that shows the resources a user can access, and a detail view that gates a single resource on a per-user permission.

The example is a small project management application. By the end, you’ll have:

  • A project resource type and a pair of roles
  • A /projects page that lists only the projects the signed-in user can access
  • A /projects/:id page that returns 404 if the user can’t view it
  • Empty state handling for users with no accessible projects

The model uses a single resource type under the organization. Deeper hierarchies are covered in the Deep Inheritance guide.

organization (implicit root)
└─ project

1. Model the hierarchy

A single resource type works well for list and detail apps where every resource is a peer. There are no parent workspaces or nested sub-projects – just projects that belong to an organization.

In the WorkOS Dashboard, navigate to Authorization > Resource Types and create one resource type:

NameSlugParent
ProjectprojectOrganization

For the full Dashboard walkthrough, see Quick Start: Configure resource types.

2. Define roles and permissions

The list and detail views need to answer two questions: can the user view this project, and can the user edit it? Create three permissions and two roles, all scoped to the project resource type.

PermissionDescription
project:viewView a project
project:editEdit a project
project:deleteDelete a project
RolePermissions
project-viewerproject:view
project-editorproject:view, project:edit, project:delete

Because the roles are scoped to project, each assignment grants access to exactly one project. For step-by-step instructions, see Quick Start: Create roles and permissions.

3. Register projects as resources

Resources should be registered as users create entities in your application. When a project is created in the database, create a matching FGA resource using the project’s database ID as the external_id. This lets your application reference projects by their own primary key without storing the WorkOS resource ID.

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

Two operations run on every project creation. The project is registered as an FGA resource so it can be referenced in access checks, and the creator is assigned project-editor on the new project so they can edit it immediately.

If the FGA call fails after the database insert, the project will exist with no one assigned to it. Use a transaction or a retry queue to keep the database and FGA in sync.

4. Grant access to other users

To share a project with a teammate, assign the appropriate role on that specific project:

curl https://api.workos.com/authorization/organization_memberships/om_02HXYZ/role_assignments \
-X POST \
-H "Authorization: Bearer sk_example_123456789" \
-H "Content-Type: application/json" \
-d '{
"role_slug": "project-viewer",
"resource_type_slug": "project",
"resource_external_id": "proj_01H"
}'

A single resource-scoped assignment grants access to that project only. See Role assignments for more on assignment management.

5. Power the list view

The list view needs the set of projects the signed-in user can see. Use the list resources for a user endpoint, filtering by project:view:

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

This is a common pattern: ask FGA for the resource IDs the user can access, then load the row data from your database. FGA is the source of truth for access, and the database is the source of truth for project content.

A React component for the list looks like this:

function ProjectList() {
const { data: projects, isLoading } = useQuery('/projects');
if (isLoading) return <Spinner />;
if (projects.length === 0) {
return (
<EmptyState
title="No projects yet"
description="Create a project or ask a teammate to share one with you."
action={<Link to="/projects/new">Create project</Link>}
/>
);
}
return (
<ul>
{projects.map((project) => (
<li key={project.id}>
<Link to={`/projects/${project.id}`}>{project.name}</Link>
</li>
))}
</ul>
);
}

The empty state covers two cases: a new user who hasn’t created anything yet, and a user who has no access to any projects.

Pagination

listResourcesForMembership returns up to 100 results per page. For longer lists, pass the after cursor from the response to the next call. Match the page size to the page size of the UI to avoid fetching IDs that won’t be rendered.

6. Power the detail view

The detail view gates the page on a single permission: project:view. Run the check before loading the project, and return 404 if the user isn’t authorized.

app.get('/projects/:projectId', async (req, res) => {
const { organizationMembershipId } = req.user;
const { projectId } = req.params;
const { authorized } = await workos.authorization.check({
organizationMembershipId,
permissionSlug: 'project:view',
resourceTypeSlug: 'project',
resourceExternalId: projectId,
});
if (!authorized) {
return res.status(404).json({ error: 'Not found' });
}
const project = await db.projects.findUnique({ where: { id: projectId } });
return res.json(project);
});

The React component renders the project. The backend has already verified that the user is allowed to view it.

function ProjectDetail() {
const { projectId } = useParams();
const { data: project, isLoading } = useQuery(`/projects/${projectId}`);
if (isLoading) return <Spinner />;
if (!project) return <NotFound />;
return (
<article>
<h1>{project.name}</h1>
<p>{project.description}</p>
</article>
);
}

Gating individual UI elements like an Edit button is covered in the Protecting API Endpoints guide.

7. Handle empty and forbidden states

Two patterns need explicit handling.

Empty list. A user with no accessible projects should see a clear call to action rather than an empty page. The list component above handles this with an empty state that suggests creating a new project.

Forbidden detail page. When a user navigates to a project they can’t view, return 404 instead of 403. A 403 response confirms that the project exists, which can leak information across organization boundaries. The detail handler returns 404 for both unauthorized requests and missing projects.

The same pattern applies to write endpoints. An edit request for a project the user can’t view should also return 404, since acknowledging that the project exists reveals information.

Putting it all together

The complete flow for the basic app:

1. Model → One resource type: project
2. Privileges → project-viewer and project-editor roles
3. Resource → Register a project on create, assign creator as editor
4. Share → Assign roles directly on the project to grant access
5. List view → listResourcesForMembership(project:view) → render rows from your DB
6. Detail view → check(project:view) → 404 if not authorized
7. Empty / 404 → Empty state for no access; 404 for forbidden detail pages

This is the smallest end-to-end FGA integration for an application with list and detail views. The other “Model your app” guides extend this shape with deeper hierarchies, high-cardinality children, and richer sharing flows.

Next steps

Other “Model your app” guides:

  • App with deep inheritance – Multi-level hierarchies where access flows through several layers (workspaces → projects → apps).
  • Deep Inheritance – Multi-level hierarchies and listEffectivePermissions. See Deep Inheritance.
  • 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:

  • Quick Start – The end-to-end reference walkthrough
  • Access Checks – JWT versus API checks and integration patterns
  • Resource Discovery – Listing accessible resources and members
  • High-Cardinality Entities – When to keep data in FGA versus the database
Deep InheritanceModel authorization for an application where users navigate through nested resources and a single detail page renders many permission-gated components
Up next
© WorkOS, Inc.
FeaturesAuthKitSingle Sign-OnDirectory SyncAdmin PortalFine-Grained Authorization
DevelopersDocumentationChangelogAPI Status
ResourcesBlogPodcastPricingSecuritySupport
CompanyAboutCustomersCareersLegalPrivacy
© WorkOS, Inc.