How to Build Dynamic Feature Toggling in React.js
- Share:
Feature toggling enables different users to see or interact with specific features based on conditions like user role, location, or relationship to content.
With the dynamic and often complex nature of modern applications, feature toggling is an essential capability for basically any application you might be building. That means that we, as software developers, need its implementation to be dynamic, intuitive, and easy to manage.
This guide will walk you through building a dynamic feature flagging system in React, allowing you to toggle features on and off based on user relationships and permissions. With a combination of CASL, a versatile JavaScript authorization library, and Permit.io for managing Relationship-Based Access Control (ReBAC), you’ll learn how to efficiently create flexible, dynamic access controls for any React app.
What We'll Build
In this tutorial, we’ll build a basic document management app that uses feature flags to control access to various document-related features. Users with specific relationships to documents—such as Document Owners or users with Category Access—will have different permissions to create, view, or edit documents. This app dynamically adapts feature visibility and actions based on user relationships, making it a perfect example of a flexible and scalable feature flagging approach in React.
Before we dive into the step-by-step instructions, let’s look at the importance of React Feature Flags and the importance of coupling them with a Fine-Grained Authorization (FGA) layer in the form of Relationship-Based Access Control (ReBAC).
The Importance of Feature Flags in React
Feature flags are critical in React apps, enabling real-time feature management without the need to deploy new code. They provide a way to test features with limited users, roll out updates gradually, or restrict features based on specific criteria. By integrating feature flags, you gain granular control over your user interface, ensuring that only relevant features are visible to each user. This enhances security, personalizes user experience, and allows for better testing and iteration cycles.
With the flexibility of CASL and the real-time ReBAC via Permit.io, we’ll build a feature flagging system that goes beyond static roles. CASL provides powerful access control logic in JavaScript, and Permit.io allows for defining permissions based on user-resource relationships rather than rigid role definitions. Combining these tools, you’ll be able to create a dynamic and highly adaptable feature flagging system that updates instantly with relationship changes.
Before We Start:
To follow this tutorial, you’ll need to create an account with Permit.io account and configure a local Policy Decision Point (PDP) with a valid API key (Which we’ll show you how to do)
The demo app code is available in this GitHub repository.
By the end, you’ll have a clear understanding of how to create dynamic feature flags that adapt based on user relationships using CASL and Permit.io in React.
Modeling ReBAC Policies
Let’s get started by defining resources
Defining Resources
For this document manager app, we will create two resources: Category and Document. Think of a category as a folder and document as files in the folder. For instance, finance is a category, and expense_budget and salary_expenses are documents in the finance category.
Next, we define actions that a user can act on the Category and Document resources.
For Category:
- list-documents, create-document, delete, and rename
For Document:
- comment, edit, delete, read
Defining Resource Roles
A resource role denotes the set of permissions that can be granted to instances of a particular resource. In our case, the Category resource will have the Editor, Admin, and Viewer roles.
The document resource will have Editor and Viewer roles.
Defining Permissions
Here, we assign what action a resource role (e.g., Category#Editor) is permitted to do.
A Category#Admin, in our case, has the permission to:
- create-document
- delete
- list-documents
- rename
A Document#Editor has permission to:
- comment
- edit
- read
- but no permission to delete
Defining Relations
In Permit, a relation is a type of relationship Tuple. In our case, we have just one relation. Category is a parent of Document. Learn more about Relationship Tuples here.
Defining Role Derivations
In ReBAC, role derivation lets users gain permissions without needing a direct role assignment for each instance. As a result, a Category#Editor will also be a Document#Editor when a Category instance is the parent of a Document instance. This means a user with a Category#Editor role has access to edit all documents in the finance category even without explicitly defining that permission.
Same with a Category#Viewer.
It’s important to note that this derivation is based on the relation. For instance, a Document#Editor can not be a Category#Editor.
This is what our model looks like so far.
Defining Resource Instances and Relationship Tuples
Let’s define some resource instances. As the name implies, a resource instance is a specific occurrence or version of a resource. In our case, we create finance and HR as resource instances of Category. Then, budget_report, marketing_expense, and salary_report are resource instances of Document.
Now let’s define relationships between these instances. As defined, the finance category has access to budget_report and marketing_expense documents. This is cool because the resource role derivation and relations have established this connection.
The HR category has access to the salary_report document
Assigning User to Resource Instance
Note: In a real-world application, the role assignment happens programmatically using Permit’s SDKs.
To assign a user to a resource instance, you have to pass the userId
, role
, tenant
, and resource_instance
to the roleAssignments
endpoint.
// Assign user as editor of a category
await permit.api.roleAssignments.assign({
user: userId,
role: "Editor", // Becomes Category#Editor
tenant: "default",
resource_instance: `Category:${categoryId}`
});
Here’s an example:
This admin user has instance access to all categories and documents in the app.
At this point, your ReBAC system is ready.
However, the goal of this guide is to use this to implement feature toggling. So our next step is to write some API endpoints that use permit.check
to determine if a user can perform certain actions or have access to resources.
Setting up Local PDP and Initializing Permit SDK
In a ReBAC system, policies can become highly dynamic and context-sensitive, relying on various attributes, roles, and relationships between users and resources.
To evaluate these policies effectively, a Policy Decision Point (PDP) is required. The PDP is responsible for making real-time decisions on whether a user is allowed to perform certain actions on a resource based on the relationships and rules defined within the access control system.
For this project, we use a Local PDP to handle the more complex ReBAC policies.
You can follow Permit.io's official documentation to set up your Local PDP using Docker. Below is a screenshot showing the steps to pull and run the PDP container:
Once the PDP container is running, it will listen on port 7766
and will handle incoming requests for permission checks. The PDP container communicates with Permit.io's API and processes the ReBAC policies that define access control based on user-resource relationships.
Next, in our project, we connect the backend to this PDP. This is done in the permitInstance.js
file where the Permit.io SDK is initialized:
require('dotenv').config({ path: ".env" });
const { Permit } = require("permitio");
const permit = new Permit({
token: process.env.PERMIT_TOKEN,
pdp: "<http://localhost:7766>",
debug: true,
log: {
level: "debug"
}
});
module.exports = permit;
This setup ensures that any permission checks made through the backend will now be processed by the local PDP.
Creating API endpoints to handle permission checks
We need to specify some endpoints to check if a specific user has the necessary permissions to perform a particular action on a given resource.
This is where the advantage of ReBAC really shines through - instead of explicitly assigning access to documents per user, we can simply assign a user to the relevant category, and document access will be assigned automatically.
For the scope of this article, we’ll only check if a user has the necessary permissions to categories. However, you can go ahead and explore the endpoint implementation for documents here
router.get('/', async (req, res) => {
try {
const userId = req.headers['user-id'];
const accessibleCategories = await Promise.all(
Object.values(categories).map(async (category) => {
const canAccess = await permit.check(userId, "list-documents", `Category:${category.id}`);
if (!canAccess) return null;
// Add document count
const categoryDocuments = Object.values(documents)
.filter(doc => doc.categoryId === category.id);
return {
...category,
documentCount: categoryDocuments.length
};
})
);
res.json(accessibleCategories.filter(Boolean));
} catch (error) {
res.status(500).json({ error: error.message });
}
});
This endpoint determines which categories a user has access to by using permit.check to verify permissions based on relationships. This endpoint returns only the categories where the user has the list-documents
permission, along with a count of documents in each accessible category.
Integrating CASL with permit-fe-sdk
CASL allows us to enforce granular permissions within our app, ensuring that resources and actions are accessible only to authorized users or roles.
For our React frontend, we use permit-fe-sdk to implement permission-based feature toggling. Instead of making individual permission checks, we load all necessary permissions when the app initializes using loadLocalStateBulk
. This optimizes performance by reducing the number of API calls to our permission server.
// lib/permit.ts
import { Permit, permitState } from 'permit-fe-sdk';
export const getAbility = async (userId: string) => {
console.log('Initializing permit for user:', userId);
const permit = Permit({
loggedInUser: userId,
backendUrl: "<http://localhost:3001>"
});
await permit.loadLocalStateBulk([
// Category permissions with instance keys
{ action: "list-documents", resource: "Category:finance" },
{ action: "list-documents", resource: "Category:hr" },
{ action: "create-document", resource: "Category:finance" },
{ action: "create-document", resource: "Category:hr" },
// Document permissions with instance keys
{ action: "read", resource: "Document:budget_report" },
{ action: "read", resource: "Document:marketing_expense" },
{ action: "read", resource: "Document:salary_report" },
{ action: "edit", resource: "Document:budget_report" },
{ action: "edit", resource: "Document:marketing_expense" },
{ action: "edit", resource: "Document:salary_report" },
]);
console.log('Permissions loaded into permitState');
};
export { permitState };
Notice how we specify the exact resource instances (e.g., "Category:finance") rather than just resource types. This matches our ReBAC model where permissions are based on relationships to specific resource instances.
Conditionally Render the UI using permitState
In the Category page, we use permitState.check
to verify if the logged-in user has access to list documents in a specific category. This permission check determines whether to show the documents or display an access denied message.
// pages/categories/[categoryId].tsx
useEffect(() => {
const loadData = async () => {
if (!isLoaded || !user || !categoryId || !permissionsLoaded) return;
// Check if user can access this category
const canAccess = permitState?.check(
"list-documents",
`Category:${categoryId}`,
{},
{}
);
if (!canAccess) {
setAccessError(`You don't have access to view documents in this category`);
return;
}
// Load documents if user has access
setIsLoading(true);
try {
const docs = await fetchCategoryDocuments(categoryId as string, user.id);
setDocuments(docs);
setCategoryName(categoryId === 'finance' ? 'Finance' : 'HR');
} catch (error) {
console.error('Error loading documents:', error);
} finally {
setIsLoading(false);
}
};
loadData();
}, [categoryId, isLoaded, user, permissionsLoaded]);
When a user navigates to a category page, we check their permission before loading any data. If they don't have access, they see an error message instead of the documents list.
Conclusion
ReBAC with Permit.io provides precise, relationship-based control over feature access in our document management app. Unlike traditional role-based systems, permissions are dynamically determined by the relationships between users and resources - like being an editor of a specific category or viewer of certain documents.
Using the permit-fe-sdk, we implemented these complex permission checks with simple, intuitive code. The ability to load permissions in bulk and check them using permitState.check()
makes it straightforward to toggle features based on user relationships, without compromising on security or performance.
This approach shines in scenarios where access control needs to be granular and dynamic, making it an excellent choice for modern applications where user permissions need to adapt based on context and relationships.
Want to learn more about implementing authorization? Got questions?
Reach out to us in our Slack community.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker