Add RBAC & ABAC to a Webflow App Using PermitIO
Modern web applications require sophisticated authorization systems that go beyond simple “logged in” or “logged out” states. Whether you’re building a content management system, a collaborative workspace, or an enterprise application, you need to control who can access what resources and perform which actions. This is where authorization comes into play.
The challenge lies in implementing these authorization systems in Webflow, a no-code platform that doesn’t natively support server-side authorization logic. Traditional approaches require building custom authentication servers and managing complex permission states, often leading to security vulnerabilities or inconsistent authorization enforcement. Webflow’s client-side nature means authorization logic needs careful architecture to prevent tampering while maintaining seamless user experience.
In this tutorial, you’ll learn how to implement both RBAC and ABAC in a Webflow application using Permit.io, an authorization-as-a-service platform that handles the complexity of permission management. We’ll create an Event Management Dashboard for an organization where different departments organize and manage events. Our application will demonstrate:
- RBAC: Administrators can manage all events, Organizers can manage department-specific events, and Attendees can only view events
- ABAC: Users can only edit events they organized or events from their department, with additional time-based restrictions
Prerequisites
To follow this tutorial, you’ll need:
- A Permit IO account
- A Render account for deployment
- Basic understanding of JavaScript and REST APIs
- A Webflow account with a Basic site plan to work with. Visit the Webflow Marketplace to clone a copy of the web application we’ll work with for the rest of the article.
Getting Started with Permit IO
Before diving into code, we need to set up our authorization platform. Creating a Permit IO account gives you access to their dashboard, where you’ll define your authorization model visually before implementing it in code.
After signing up at Permit IO, create a new project called “Event Management.” This project will house all our authorization configurations. Navigate to the “Projects” page, and click on the “New Project” button. Remember to switch to the Development environment — this is where we’ll set up our entire authorization model before promoting it to production.
Now, the first step is defining what we’re protecting. In Permit IO, these are called “resources.” For our event management system, we have one primary resource: Events. Navigate to the Policy tab and create an Event resource with these actions: create, read, update, delete, and publish. This gives us fine-grained control over what operations users can perform on events.
Next, we need to establish our roles. Click on the Roles tab and create three roles that reflect our organizational structure: Administrator (full access), Organizer (can manage events in their department), and Attendee (read-only access). These roles form the foundation of our RBAC implementation.
Now comes the critical part: connecting roles to permissions. In the Policy Editor, you’ll see a matrix where roles meet actions. Check the appropriate boxes — Administrators get all actions on Events, Organizers get create, read, update, publish, while Attendees only get read access. This simple yet powerful configuration is the heart of RBAC.
Building the Authorization Backend
Unlike traditional approaches where we’d build the authorization system from scratch, for this tutorial, we’re starting with a pre-built backend service that’s already been deployed and tested. This approach lets us focus on integrating authorization into your Webflow application rather than spending time building infrastructure.
The backend service codebase we’ll be using is available on GitHub at permitio/webflow-permit-tutorial. This Express.js server acts as a secure bridge between your Webflow frontend and Permit IO’s Policy Decision Point (PDP). By having this intermediary layer, we protect your API keys and ensure authorization checks happen server-side where they can’t be tampered with.
Here’s what the backend project structure looks like:
permit-io-webflow/
├── Dockerfile
├── index.html
├── package.json
├── package-lock.json
└── src/
└── index.jsThe beauty of this setup is its simplicity. The index.js file contains all the routes you'll need for RBAC and ABAC operations:
/api/auth/sync-user- Registers users with Permit IO/api/auth/check-permission- Checks basic role permissions (RBAC)/api/auth/assign-role- Assigns roles to users/api/auth/check-context-permission- Handles attribute-based checks (ABAC)/api/auth/get-user-attributes- Retrieve user attributes for ABAC
Both the authorization backend and PDP are already deployed and running:
- Backend API:
https://permit-io-webflow-1.onrender.comon Render - PDP Service: The Permit.io team provides a managed Cloud PDP at
https://cloudpdp.api.permit.iowhich is available globally. You can as well deploy your instance using the Docker image provided on their website.
You can use these endpoints directly for development, or clone the repository to deploy your own instances:
git clone https://github.com/Ikeh-Akinyemi/permit-io-webflow.git
cd permit-io-webflowIf you choose to deploy your own instance, you’ll need to:
- Get your API key from the Permit IO dashboard (Projects → Your Project → Development → Copy API Key)
- Update environment variables:
PERMIT_API_KEY: Your Permit IO API keyPDP_URL: URL of your deployed PDP service (if deploying separately)
The backend uses a standard Express.js setup with the Permit IO SDK integrated. All authentication routes are protected with CORS to ensure they can only be called from your Webflow domain, adding an extra layer of security to your implementation.
Let me continue with the implementation section, focusing on how we’ll integrate authorization into Webflow using data attributes instead.
Implementing Authorization in Webflow
Since Webflow operates primarily in the browser, we need a practical approach to implement authorization that works within Webflow’s constraints. We’ll use custom data attributes to mark elements that require permission checks.
Setting Up the Authorization Script
First, let’s add our authorization script to the Webflow project. Navigate to your site settings on the dashboard, click on the “Custom Code” tab, and add the following script to the “Head Code” section:
<script>
document.addEventListener('DOMContentLoaded', function () {
// References to elements
const signinButton = document.querySelector('.signin-button');
const signinButtonContainer = document.querySelector('.sign_in_container');
const signinDialog = document.querySelector('.signin-dialog');
const formContainer = document.querySelector('.form-container');
const emailForm = document.querySelector('form');
const successMessage = document.querySelector('.success-message .s-text');
const errorMessage = document.querySelector('.error-message .e-text');
const roleDeptWrapper = document.querySelector('.role-dept-wrapper');
const createEventBtn = document.querySelector('.create-event-btn');
const cards = document.querySelector('.card_x'); // there's two of this class.
// API endpoint
const API_URL = 'https://permit-io-webflow-1.onrender.com';
// Check if user is logged in
const userEmail = sessionStorage.getItem('user_email');
if (userEmail) {
showAuthorizedUI(userEmail);
} else {
hideAuthorizedElements();
}
</script>This initial code sets up the basic structure. When the page loads, it checks if a user email exists in session storage. If it does, the user is considered logged in, and we’ll show the appropriate UI elements based on their permissions.
Creating the Authentication Flow
Next, let’s implement the basic authentication flow. Add a hidden class to all elements within the Webflow project except the header containing the “Sign in” button. Then, set up a dialog containing a form with an email input and a dropdown element with name=”department”. Add options for different departments (Marketing, Sales, Engineering):
To complete the sign in workflow, add the following to your script, right after the previous code:
// Show signin dialog when button is clicked
signinButton.addEventListener('click', function() {
signinDialog.style.display = 'block';
});
// Handle form submission
emailForm.addEventListener('submit', function(e) {
e.preventDefault();
const emailInput = emailForm.querySelector('input[type="email"]');
const email = emailInput.value.trim();
// Get the selected role from the dropdown
const departmentSelect = emailForm.querySelector('select');
const department = departmentSelect.value;
if (!email) {
showError('Please enter a valid email address');
return;
}
authenticateUser(email, department);
});This code handles the sign-in flow by displaying the dialog when the sign-in button is clicked and processing the form submission.
Let’s define the authenticateUser function that will communicate with our Permit IO backend:
// Authenticate user with Permit IO
async function authenticateUser(email, department) {
try {
// Show loading state
formContainer.classList.add('loading');
// Sync user with Permit IO
const response = await fetch(`${API_URL}/api/auth/sync-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId: email, email, department })
});
const data = await response.json();
if (data.success) {
// Store user email in session
sessionStorage.setItem('user_email', email);
// Show success message
showSuccess('Authentication successful!');
// Close dialog after 1 second
setTimeout(() => {
signinDialog.style.display = 'none';
showAuthorizedUI(email);
}, 300);
} else {
showError('Authentication failed. Please try again.');
}
} catch (error) {
console.error('Authentication error:', error);
showError('Authentication failed. Please try again.');
} finally {
formContainer.classList.remove('loading');
}
}This function makes a POST request to our backend to sync the user with Permit IO. If successful, it stores the user’s email in session storage and updates the UI.
Managing Authorized UI Elements
Now, let’s implement the functions to show and hide UI elements based on the user’s permissions. First, the function to hide elements when no user is logged in:
// Hide elements that require authorization
function hideAuthorizedElements() {
// Hide elements with data-permission attribute
document.querySelectorAll('[data-permission]').forEach(element => {
element.style.display = 'none';
});
// Hide elements
const roleDeptWrapper = document.querySelector('.role-dept-wrapper');
if (roleDeptWrapper) {
roleDeptWrapper.classList.add('hidden');
}
const createEventBtn = document.querySelector('.create-event-btn');
if (createEventBtn) {
createEventBtn.classList.add('hidden');
}
const cards = document.querySelectorAll('.card_x');
cards.forEach(card => {
card.classList.add('hidden');
});
// Make sure the signin button is visible
signinButton.style.display = 'block';
}Next, the function to show elements based on the user’s permissions:
// Show elements based on user permissions
async function showAuthorizedUI(email) {
// Hide signin button and login email
signinButton.style.display = 'none';
signinButtonContainer.textContent = email;
// Remove hidden class elements
const roleDeptWrapper = document.querySelector('.role-dept-wrapper');
if (roleDeptWrapper) {
roleDeptWrapper.classList.remove('hidden');
}
const createEventBtn = document.querySelector('.create-event-btn');
if (createEventBtn) {
createEventBtn.classList.remove('hidden');
}
const cards = document.querySelectorAll('.card_x');
cards.forEach(card => {
card.classList.remove('hidden');
});
// Check permissions and show appropriate elements
await checkAndApplyPermissions(email);
}The key to our RBAC implementation is the checkAndApplyPermissions function:
// Check user permissions and apply to UI
async function checkAndApplyPermissions(email) {
try {
// Get all elements with permission requirements
const permissionElements = document.querySelectorAll('[data-permission]');
// Check each element's permission
for (const element of permissionElements) {
const action = element.dataset.permission;
const resource = element.dataset.resource || 'Event';
// Check permission with Permit IO
const response = await fetch(`${API_URL}/api/auth/check-permission`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userId: email,
action: action,
resource: resource
})
});
const data = await response.json();
// Show/hide element based on permission
element.style.display = data.allowed ? 'block' : 'none';
}
} catch (error) {
console.error('Permission check error:', error);
}
}Finally, add the helper functions for showing success and error messages:
// Show success message
function showSuccess(message) {
successMessage.textContent = message;
successMessage.parentElement.style.display = 'block';
errorMessage.parentElement.style.display = 'none';
}
// Show error message
function showError(message) {
errorMessage.textContent = message;
errorMessage.parentElement.style.display = 'block';
successMessage.parentElement.style.display = 'none';
}
}); // End of DOMContentLoadedAdding Permission Attributes to Webflow Elements
Now that our script is ready, we need to mark elements in our Webflow design with data attributes to control their visibility based on user permissions.
For each element that should be controlled by permissions, add the following data attributes:
data-permission: The action required (create, read, update, delete, publish)data-resource: The resource type (defaults to "Event" if not specified)
Here’s how to add these attributes in Webflow:
- Select the element you want to restrict
- Click on Settings in the right panel
- Scroll down to the “Custom Attributes” section
- Add a new attribute with the name
data-permissionand a value likecreate,update, ordelete - Optionally add another attribute named
data-resourcewith the valueEvent
For example, to restrict the “Delete Event” button to users with delete permission:
- Select the delete button
- Add custom attribute:
data-permission="delete" - Add custom attribute:
data-resource="Event"
Repeat this process for all elements that should be permission-controlled:
- “Create Event” button:
data-permission="create" - “Edit Event” button:
data-permission="update" - “Publish Event” button:
data-permission="publish"
With these attributes in place, our script will automatically handle showing and hiding elements based on the user’s permissions. Here’s what a user with Attendee role will see:
Likewise, a user with Administrator role will see all elements as below:
Implementing ABAC for Department-Based Access
Now that we have RBAC working, let’s enhance our authorization with Attribute-Based Access Control (ABAC). ABAC allows for more nuanced permissions based on attributes like department, ownership, or time of day.
For our Event Management dashboard, we want to implement access control where users can only see events from their department.
Adding Attributes to Elements in Webflow First, we need to add department attributes to our event elements. For each event in your Webflow CMS or design:
- Select the event container element
- Add custom attribute:
data-departmentwith values likeMarketing,Sales, orEngineering
For example, for a Marketing event:
- Select the event container
- Add custom attribute:
data-department="Marketing"
Now modify the department button-like selector below the head section to include data-department for each department:
This combination tells our script to check both role-based permissions and attribute-based conditions.
Extending Our Script for ABAC
Next, let’s enhance our authorization script to handle attribute-based checks. Add the following function to your script, after the checkAndApplyPermissions function:
// Check department-based permissions for elements
async function checkAttributeBasedPermissions(email) {
try {
// Get user department from Permit IO
const userResponse = await fetch(`${API_URL}/api/auth/get-user-attributes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId: email })
});
const userData = await userResponse.json();
const userDepartment = userData.attributes?.department || '';
// this handles administration.
if (userDepartment == "*") return.
// Find all elements with data-department attribute
const departmentElements = document.querySelectorAll('[data-department]');
// Hide elements that don't match user's department
departmentElements.forEach(element => {
const elementDepartment = element.dataset.department;
if (elementDepartment && elementDepartment !== userDepartment) {
element.style.display = 'none';
}
});
} catch (error) {
console.error('ABAC check error:', error);
}
}This function handles our attribute-based access control by retrieving the user’s department from Permit IO. Then finds all elements marked for ABAC checks, and determine each element’s associated department. It also checks if the user has permission based on these attributes, consequently showing or hiding elements accordingly.
Now, update the showAuthorizedUI function to include ABAC checks:
async function showAuthorizedUI(email) {
...
// Check attribute-based permissions
await checkAttributeBasedPermissions(email);
}With the new ABAC rules, Attendee see only the below UI setup:
While the Administrator user who has full access across the entire department will see the below UI:
Testing ABAC with Department Permissions To test the access controls that we have implemented in this Webflow project:
- Create an “admin@example.com” user with attribute set to
departmet: "*" - Create an “attendee@example.com” user with its department set to “Sales”
- Log in with admin email and verify:
- All the UI appeared for the user.
- Create, Delete, and Edit buttons are showing for this user.
- Log in with attendee email and verify:
- This user can only view events from their department with the create, edit, or delete buttons hidden.
Conclusion
In this tutorial, we’ve successfully implemented a comprehensive authorization system for a Webflow application using Permit IO. By combining RBAC and ABAC, we’ve created a flexible, powerful access control system that can adapt to complex organizational needs.
The key advantages of our approach include:
- Separation of concerns: Our authorization logic is maintained in Permit IO, separate from the application UI, making it easier to update and maintain.
- Layered security: By implementing both RBAC and ABAC, we’ve created multiple security layers that work together to provide precise access control.
- Client-side implementation: Despite Webflow’s limitations, we’ve built a robust authorization system using client-side JavaScript and data attributes, proving that permission systems are possible in no-code platforms.
The key advantage of using Permit IO is that it separates authorization logic from your application code, making it easier to manage and update permissions without redeploying your application. As your application grows, you can easily extend this system with new roles, attributes, and policies. You can find the final version of the deployed Webflow project here.
For more advanced use cases, explore Permit IO’s documentation on ReBAC (Relationship-Based Access Control) and custom policies. You can also join the Permit IO community to discuss authorization patterns and best practices with other developers.
