Permit logo
Home/Blog/

How to Build Dynamic Feature Toggling in React.js

A guide on how to build a React Feature Toggling system. Discover how to use CASL and Permit.io for relationship-based access control (ReBAC) to manage feature visibility dynamically
 How to Build Dynamic Feature Toggling in React.js
Gabriel L. Manor

Gabriel L. Manor

|
  • 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.

image (70).png

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
image (71).png

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.

image (72).png

The document resource will have Editor and Viewer roles.

image (85).png

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
image (73).png

A Document#Editor has permission to:

  • comment
  • edit
  • read
  • but no permission to delete

    image (74).png

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.

image (75).png

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.

image (76).png

Same with a Category#Viewer.

image (77).png

It’s important to note that this derivation is based on the relation. For instance, a Document#Editor can not be a Category#Editor.

image (78).png

This is what our model looks like so far.

image (79).png

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.

image (80).png

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.

image (81).png

The HR category has access to the salary_report document

image (82).png

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:

image (83).png

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:

image (84).png

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

Gabriel L. Manor

Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker

Test in minutes, go to prod in days.

Get Started Now

Join our Community

2026 Members

Get support from our experts, Learn from fellow devs

Join Permit's Slack