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

If you’re working on a medium or large Vue 3 project, sooner or later, someone’s going to ask, Can we hide the settings page from normal users? or How come editors see the admin stuff? I’ve been there copy–pasting v-if checks all over the place, and honestly, it’s a mess.
So, I finally decided to set up real role-based access control (RBAC). Here, I’ll show you an easy way to set up role-based access control, so users only see what they’re allowed to see.
What is RBAC?
RBAC (role-based access control) just means you group your users by roles, means only certain people can see or perform certain things Like:
Admin: Can basically do everything
Editor: Can mess with content, but not settings
Viewer: Can only look, not change anything
RBAC helps you keep your app neat and secure, since all the rules live in one place.
Step 1. Setup: New Project, Fresh Folders
Open your terminal and run below commands to create and setup a new project:
npm create vue@latest vue-rbac-app
cd vue-rbac-app
npm install
npm install vue-router@4
I like keeping things cleaner. Here’s a good structure:
src/
views/
router/
composables/
components/
Step 2. Routing
Here’s my src/router/index.js
. Note the meta
field with roles!
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: ["admin", "editor"]
}
},
{
path: "/unauthorized",
name: "Unauthorized",
component: NotAuthorized
}
];
const router = createRouter(
{
history: createWebHistory(),
routes
}
);
export default router;
I use createWebHistory()
because who wants #/
in their URLs.
Step 3. Fake Auth for Testing (No Backend Drama Yet)
Before anyone logs in for real, I just fake my users with a composable.
Make a file, src/composables/useAuth.js
:
export function useAuth() {
// Change the role to try different stuff!
return { isAuthenticated: true, role: 'editor' }; // try admin/viewer
}
Step 4. Setting Up Route Guards
Here comes the fun: before any page loads, check if they’re allowed.
Edit src/main.js
:
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { useAuth } from "./composables/useAuth";
const app = createApp(App);
// Global navigation guard to check authentication and authorization
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");
Heads up: This is dead simple for now. Replace with a real store/hooks as you get fancy.
Step 5. Make Some Pages to Test
Just basic views in /src/views/
:
Home.vue
<template>
<h1>
Welcome to Home Page
</h1>
</template>
Admin.vue
<template>
<h1>
Admin Dashboard For Admins Only
</h1>
</template>
Editor.vue
<template>
<h1>
Editor Page - Editors and Admins
</h1>
</template>
NotAuthorized.vue
<template>
<h1>
Access Denied—You’re Not Allowed Here
</h1>
</template>
Step 6. Only Show Buttons For Right Roles
Let’s say only admins can delete stuff.
components/DeleteButton.vue
<template>
<button v-if="role === 'admin'">
Delete Post
</button>
</template>
<script>
import { useAuth } from '../composables/useAuth';
export default {
name: 'DeleteButton',
computed: {
// get user role from the auth composable
role() {
return useAuth().role;
}
}
};
</script>
Step 7. Smarter Sidebar (No More Dead Links)
No one likes seeing a menu link that just gives you “Access Denied.”
Here’s my basic trick:
const sidebarItems = [
{
name: "Home",
path: "/"
},
{
name: "Admin Panel",
path: "/admin",
roles: ["admin"]
},
{
name: "Edit Articles",
path: "/editor",
roles: ["admin", "editor"]
}
];
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>
Step 8. Cleaner Role Checks—A <HasRole /> Wrapper
Let’s not repeat ourselves. Here’s a wrapper for role-only content:
components/HasRole.vue:
<template>
<slot v-if="hasRole" />
</template>
<script>
import { useAuth } from '../composables/useAuth';
export default {
name: 'RoleWrapper',
props: {
roles: {
type: Array,
required: true
}
},
data() {
return {
user: useAuth() // get current user info
};
},
computed: {
// check if user's role is in the allowed roles
hasRole() {
return this.roles.includes(this.user.role);
}
}
};
</script>
How to use:
<HasRole :roles="['admin']">
<button>Delete Post</button>
</HasRole>
One line and done.
What Next?
Connect real auth: Firebase, Auth0, or your own backend (Pinia/Vuex to save state)
Add tests: Because something always breaks
Move role logic out of the router: If things get big, split it up
I tried this pattern in both Vue 2 and 3, and it holds up surprisingly well. Just don’t rely on hardcoding roles in production.
FINAL THOUGHTS
Look, RBAC isn’t rocket science. But getting it right early will save you SO much pain as your app grows. Spend 30 minutes setting this up—future-you will be grateful.
Subscribe to my newsletter
Read articles from Krishna Akbari directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
