Implementing Multi-Tenant RBAC in Nuxt.js
- Share:
Nuxt.js, a Vue-based framework, provides built-in features for building applications with server-side rendering, static site generation, and API integrations. However, it lacks built-in access control, requiring developers to implement custom authorization logic.
As your Nuxt application grows in complexity, managing user permissions becomes more challenging (especially when those apps serve multiple organizations or regions). This is where multi-tenant Role-Based Access Control (RBAC) comes in: it allows you to define what users can do, not just based on their role, but also within the context of their organization or domain.
In this tutorial, we’ll walk through how to implement multi-tenant RBAC in a Nuxt.js application using Permit.io—an authorization-as-a-service platform that simplifies role management, permission enforcement, and API protection. We’ll build a food delivery system where users are scoped to different cities (our tenants), and each user type—customer, vendor, rider, or admin—has specific permissions within their city.
You’ll learn how to:
- Model multitenancy using city-based tenant isolation
- Set up and enforce RBAC policies
- Connect Vue frontend components to server-side authorization in Nuxt
- Synchronize users and roles dynamically
- Protect routes and actions both in the frontend and backend
Whether you're working on a SaaS product or regional service platform or simply want to build secure, maintainable permissions for your app, this guide will give you a full-stack pattern for doing so in Nuxt.js.
Let’s begin by exploring the fundamentals of multitenancy in authorization.
The Importance of Multitenancy in Authorization
Multitenancy allows data and resources to be partitioned into isolated tenants, ensuring that users from one organization cannot access data belonging to another.
In our food delivery application, each city functions as a tenant. This ensures that customers, vendors, and riders only interact with orders and meals within their respective locations.
To help us implement multitenant authorization, we will use Permit.io. Every project in Permit comes with a default tenant, while additional tenants can be created to define role-based policies within different organizational units. This means:
- Roles and permissions are assigned within the context of a tenant.
- Users can have different roles in different tenants.
- Each tenant maintains its own isolated authorization rules.
More details on configuring multi-tenant authorization can be found here.
What We Will Build
The example project is a food delivery system with the following roles and actions:
- Customer: Can create an order.
- Vendor: Can fulfill an order.
- Rider: Can deliver an order.
- Admin: Can assign riders to orders.
To make the project more interesting, let’s assume that we run the food service in various cities in the US.
The service operates across multiple cities, making multi-tenancy a critical part of authorization.
The Nuxt codebase is available in this GitHub Repository.
The demo application uses Pinia for state management, tailwindcss for styling, and PrimeVue for UI components. We've set up and added these libraries as modules, which you can see in the nuxt.config.ts
file:
import Aura from '@primevue/themes/aura';
// <https://nuxt.com/docs/api/configuration/nuxt-config>
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
modules: ['@pinia/nuxt', '@nuxtjs/tailwindcss', '@primevue/nuxt-module'],
css: ['~/assets/main.css', 'primeicons/primeicons.css'],
primevue: {
autoImport: true,
options: { ripple: true, theme: { preset: Aura } }
}
});
Application Structure
A repo with the full application code is available here.
Frontend
The demo food delivery app is organized around four distinct user roles— customer
, vendor
, rider
, and admin
—each with its own dedicated route and page. These views are linked through a shared navigation bar (/components/AppMenu.vue
) that uses PrimeVue icons, Tailwind CSS for layout, and animated effects to enhance the user experience.
In addition to navigation, the sidebar includes a city switcher (which acts as a tenant selector) and a modal for managing user roles, enabling direct interaction with Permit.io for role assignments.
City selection is managed via a Pinia store (/stores/city.ts
), which maintains the current city as a reactive reference and persists it using browser local storage. This acts as the tenant context for the entire app, influencing both frontend behavior and server-side authorization logic. While cities are hardcoded in this demo for simplicity, the pattern can be extended to dynamic tenant management from a backend.
Backend
On the backend, Nuxt’s server routes—powered by Nitro and h3—handle CRUD operations for orders and meals. These routes live in the /server/routes
folder and map directly to RESTful endpoints. Instead of a real database, resource data is read from and written to JSON files via helper functions in /server/utils/database.ts
, allowing us to focus on RBAC enforcement without database setup.
The frontend interacts with these server routes using $fetch
within Pinia stores. The meals
and orders
stores wrap all network operations and manage state updates and error handling. All API requests include the current user and city in the request headers, which later feed into Permit.io checks. A shared OrdersDisplay.vue
component renders the data and provides role-based actions like fulfilling, delivering, or assigning orders, laying the foundation for permission enforcement in later steps of the tutorial.
Having an overview of how Nuxt/Vue are connected to the frontend and server, we can now proceed to implement RBAC, continuously sync users, and enforce permissions in Nuxt.
Planning RBAC Policies
Planning out roles and resources is the first step in building an RBAC implementation. Our project will focus on various user types represented by roles. The resources in this context are Meal
s and Order
s. In your product, you’ll plan this implementation based on your application’s core features.
- Roles:
customer
,vendor
,rider
,admin
- Resources:
Meal
,Order
- Actions:
- Customers can
create
orders. - Vendors can
fulfill
orders. - Riders can
deliver
orders. - Admins can
assign
riders to orders.
How to set up a Permit.io Project
Before integrating authorization into your application, you’ll need to set up a project in Permit.io and define your access control model. This involves outlining your app’s resources, roles, and the actions those roles can perform.
When you first log into the Permit UI, you’ll be prompted to create a workspace and then a project. A project serves as a container for everything related to a specific application—its users, tenants, roles, policies, and resources. Each project comes with a default tenant, but you can define additional tenants (like different cities in our app) to scope role assignments and permissions appropriately. All RBAC decisions in Permit are tenant-aware by design.
The policy editor in Permit is divided into intuitive sections: Resources, Roles, and Policies. You start by defining your resources and the actions available on each one (e.g., "fulfill" an order or "create" a meal). Then, you define roles, and finally, you configure your access policies by toggling which actions each role is allowed to perform. These settings form the basis for the RBAC checks Permit.io will perform at runtime.
This visual, structured approach to defining authorization logic not only makes permission management easier—it also improves security, supports dynamic updates, and saves development time. While this guide focuses on RBAC, Permit also supports more advanced access control models like Attribute-Based Access Control (ABAC) and Relationship-Based Access Control (ReBAC).
Once your policies are in place, you’ll still need to connect your app’s codebase to Permit so the authorization rules can be enforced in real time. We’ll cover that next, starting with installing and configuring the SDK in Nuxt.
How to set up Permit.io in Nuxt
To integrate Permit.io with your Nuxt project, start by installing the SDK:
npm install permitio
Then, configure Nuxt to recognize the package by adding it to the build.transpile
array in nuxt.config.ts
. You’ll also need to expose your Permit token and Policy Decision Point (PDP) URL via runtimeConfig
, which pulls from a local .env
file:
// In nuxt.config.ts
export default defineNuxtConfig({
// ... other properties
build: { transpile: ['permitio'] },
runtimeConfig: {
permitToken: process.env.PERMIT_TOKEN,
permitPdp: process.env.PERMIT_PDP
}
});
PERMIT_TOKEN=permit_key_XXXXXXXXXXXXXXXXXXXXXXXXX
PERMIT_PDP=https://cloudpdp.api.permit.io
Your project’s Permit token can be obtained from the project settings in the console UI.
Permit’s PDP is the engine that evaluates authorization decisions. While the public cloud PDP is fine for basic RBAC use cases, self-hosting a PDP (via Docker) is recommended for advanced strategies like ABAC or ReBAC.
With your config in place, create a reusable Permit instance in your Nuxt server code.
We can sync users to Permit and use Permit to enforce permission checks. To do this, we need a readily available instance of the Permit class in the server code. These allow us to assign roles and call the check function.
In this demo application, we are exporting the Permit instance from the /server/utils
directory.
// In /server/utils/permit.ts
import { Permit } from 'permitio';
const config = useRuntimeConfig(); // using Nuxt runtime to get env vars
export const permit = new Permit({
pdp: config.permitPdp,
token: config.permitToken
});
How to Continuously Sync Users from Nuxt to Permit
Your app will constantly onboard users—from sign-up flows, invites, or integrations. The best time to sync a user with Permit is during account creation, but you should also update Permit whenever users are modified or deleted.
Permit’s SDK offers built-in methods for managing users, roles, and tenants. You can use permit.api.users.sync
, permit.api.users.delete
, and permit.api.roleAssignments.assign
to keep Permit’s user model aligned with your app. Alternatively, use the Permit REST API directly for custom workflows.
In this tutorial, we’ve set up a demo endpoint at /server/routes/users.post.ts
to grant or revoke roles when a user interacts with the "Manage User" modal in the UI:
// In /server/routes/users.post.ts
export default defineEventHandler(async (event) => {
const { city: tenant, user, role, isGrant } = await readBody(event);
// Create User with Permit if not existing
await permit.api.users.sync({ key: user });
// Assign or Unassign Role depending on isGrant
if (isGrant) {
await permit.api.roleAssignments.assign({ user, role, tenant });
} else {
await permit.api.roleAssignments.unassign({ user, role, tenant });
}
});
This example uses the current city as the tenant. It also includes fallback logic to create users or tenants if they don’t yet exist. In production, add validation and error handling around these calls. Before testing this flow, make sure you've already created tenants like California
and Washington
in your Permit project.
Permit also supports user metadata. When creating or updating users, you can include emails, names, and attributes to enrich audit logs and support advanced models like ABAC and ReBAC.
How to Enforce Permissions in Nuxt
Once users and roles are synced, you need to enforce access control in your application. In Nuxt, the ideal place for permission checks is server middleware.
Use the globally available permit
instance (exported from /server/utils/permit.ts
) to call permit.check()
. This method takes the user ID, action, and a resource object that includes the resource type and tenant:
permit.check(user, action, { type, tenant });
In the demo app, we’ve created a generic middleware file at /server/middleware/permissions/check-role.ts
to enforce role-based permissions for meal and order operations:
export default defineEventHandler(async (event) => {
// Don't process if the path doesn't start with '/order' or '/meal'
if (!event.path.startsWith('/order') && !event.path.startsWith('/meal')) {
return;
}
// Get user and tenant from the request headers
const { user, city: tenant } = event.node.req.headers as any;
// Obtain the action and resource from the request path
// Use a more robust mechanism in a production application
let action: string;
let resource: string;
if (event.path.startsWith('/meal')) {
resource = 'Meal';
if (event.method === 'POST') {
action = 'create';
} else if (event.method === 'DELETE') {
action = 'delete';
} else {
action = 'read';
}
} else {
resource = 'Order';
if (event.method === 'POST') {
action = event.path === '/orders' ? 'create' : event.path.split('/')[3];
} else {
action = 'read';
}
}
// Allow the user to read meals and others even if they are not logged in
if (action === 'read') return;
// Check if the user is permitted carry out the action on the resource
// that's if the user has the right role in the tenant
const permitted = await permit.check(user, action, {
type: resource,
tenant
});
// If the user is not permitted, return an unauthorized response
if (!permitted) {
return {
success: false,
message: 'You are not permitted to perform this action'
};
}
// If the user is permitted, continue with the request
// Not doing anything is okay to allow the request to proceed
});
This middleware dynamically determines the action and resource based on the request path. While this logic is simplified for demo purposes, you should break this out by route or action type in production apps.
Your frontend should also enforce UI-level restrictions. Hide unauthorized buttons and pages using a tool like CASL. It allows you to preload abilities per user and apply logic directly in Vue components. For more, check out How to Solve Authorization in Vue with CASL.
Summary
To implement Role-Based Access Control (RBAC) in a Nuxt.js app with multitenancy:
- Model roles, resources, and actions in the Permit UI
- Configure Permit in Nuxt using a reusable SDK instance
- Sync users and roles in the context of tenants (e.g., cities)
- Enforce permissions in server middleware with
permit.check()
- Restrict frontend UI using tools like CASL for a complete experience
This tutorial walks through building a full-stack food delivery service where each city acts as a tenant and users interact with resources based on their assigned roles. By combining Nuxt, Vue, and Permit.io, you get powerful, real-time authorization with minimal boilerplate—ready for production or scale.
Want to try it yourself? Get started with Permit.io or join our community to discuss authorization strategies with other developers.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker