Role-Based Access Control (RBAC) in Vue 3: A Complete Guide

If you're building a Vue 3 app and wondering how to manage different user roles — like admin, editor, and viewer — you're in the right place. In this in-depth guide, we'll explore how to implement Role-Based Access Control (RBAC) using Vue Router and Vue 3’s modern capabilities.
This guide will help you:
✅ Define roles in your routing system
✅ Guard routes dynamically with Vue Router
✅ Show/hide UI components based on user roles
✅ Build a role-aware sidebar menu
✅ Avoid common pitfalls and follow best practices
Let's dive in!
🔐 What is Role-Based Access Control (RBAC)?
Role-Based Access Control (RBAC) is a way to restrict what users can see or do based on their assigned role. Think of it as defining a set of permissions for different types of users:
Admin – full access, including user management and settings
Editor – can create and edit content
Viewer – can only read content
Instead of spreading permission logic throughout your app, RBAC helps you centralize it, making your code easier to manage and secure.
RBAC is ideal for:
Admin dashboards
Multi-user SaaS applications
Portals with role-specific navigation or features
🎯 Benefits of RBAC
Improves security by limiting access
Keeps templates and components clean
Easier to debug and maintain
Works great with modular architecture
🚀 Setting Up the Project
Create a fresh Vue 3 project:
npm create vue@latest vue-rbac-app
cd vue-rbac-app
npm install
npm install vue-router@4
Remove the boilerplate components and prepare a clean folder structure like this:
src/
views/
router/
composables/
components/
📍 Setting Up Vue Router with Role Metadata
Inside router/index.js
, define your routes with role metadata in the meta
field:
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Admin from '../views/Admin.vue'
import Editor from '../views/Editor.vue'
import NotAuthorized from '../views/NotAuthorized.vue'
const routes = [
{ path: '/', name: 'Home', component: Home },
{
path: '/admin',
name: 'Admin',
component: Admin,
meta: { requiresAuth: true, roles: ['admin'] }
},
{
path: '/editor',
name: 'Editor',
component: Editor,
meta: { requiresAuth: true, roles: ['editor', 'admin'] }
},
{
path: '/unauthorized',
name: 'Unauthorized',
component: NotAuthorized
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
This makes your routing config double as your access control config.
🧪 Mocking User Authentication
In real apps, you’ll use OAuth, JWT, Firebase, etc. For now, simulate authentication using a composable.
composables/useAuth.js
export function useAuth() {
const user = {
isAuthenticated: true,
role: 'editor' // Try 'admin' or 'viewer' to test
}
return user
}
This gives us a centralized, testable mock of the current user.
⚖️ Adding Route Guards
In main.js
, intercept route changes and apply your access rules:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { useAuth } from './composables/useAuth'
const app = createApp(App)
router.beforeEach((to, from, next) => {
const user = useAuth()
if (to.meta.requiresAuth) {
if (!user.isAuthenticated) return next('/')
if (to.meta.roles && !to.meta.roles.includes(user.role)) {
return next('/unauthorized')
}
}
next()
})
app.use(router)
app.mount('#app')
These guards enforce access control globally, before each route loads.
🧱 Basic Views for Each Role
Create simple pages to test access.
views/Home.vue
<template>
<h1>🏠 Welcome to Home Page</h1>
</template>
views/Admin.vue
<template>
<h1>🛠️ Admin Dashboard - For Admins Only</h1>
</template>
views/Editor.vue
<template>
<h1>✏️ Editor Page - Editors and Admins</h1>
</template>
views/NotAuthorized.vue
<template>
<h1>🚫 Access Denied - You’re Not Allowed Here</h1>
</template>
🎯 Role-Based UI Rendering
Sometimes, you want to conditionally show components.
DeleteButton.vue
<template>
<button v-if="role === 'admin'">Delete Post</button>
</template>
<script setup>
import { useAuth } from '../composables/useAuth'
const { role } = useAuth()
</script>
For multiple roles:
<template>
<button v-if="['admin', 'editor'].includes(role)">Edit Post</button>
</template>
🧭 Building a Role-Aware Sidebar
Only show links the current user can access:
const sidebarItems = [
{ name: 'Home', path: '/' },
{ name: 'Admin Panel', path: '/admin', roles: ['admin'] },
{ name: 'Edit Articles', path: '/editor', roles: ['editor', 'admin'] }
]
const user = useAuth()
const visibleLinks = sidebarItems.filter(item => {
return !item.roles || item.roles.includes(user.role)
})
Sidebar.vue
<template>
<ul>
<li v-for="link in visibleLinks" :key="link.path">
<router-link :to="link.path">{{ link.name }}</router-link>
</li>
</ul>
</template>
🧩 Optional: <HasRole />
Wrapper Component
Avoid repeating role-checking logic by wrapping components.
components/HasRole.vue
<template>
<slot v-if="hasRole" />
</template>
<script setup>
import { computed } from 'vue'
import { useAuth } from '../composables/useAuth'
const props = defineProps({ roles: Array })
const { role } = useAuth()
const hasRole = computed(() => props.roles.includes(role))
</script>
Usage:
<HasRole :roles="['admin']">
<button>Delete Post</button>
</HasRole>
📦 Next Steps
To take this further:
Integrate Firebase/Auth0 login
Store auth state in Pinia or Vuex
Use lazy loading with route guards
Add layout components for role-specific views
Write unit tests for route access
Add TypeScript for stronger role validation
📌 Final Thoughts
Role-Based Access Control isn't just about security — it's about maintaining clean structure, avoiding bugs, and scaling your application easily. The earlier you adopt RBAC, the better your codebase will handle complex user flows.
Once you grasp these patterns, you can apply them to any app — Vue, React, Angular, or even full-stack projects.
Thanks for reading!
Subscribe to my newsletter
Read articles from Krishna Akbari directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
