# 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:

```bash
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:

```xml
src/
  views/
  router/
  composables/
  components/
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753011538565/c4b0bb69-a1e2-4371-8dd8-43ac3ebda8bc.png align="left")

## Step 2. Routing

Here’s my `src/router/index.js`. Note the `meta` field with roles!

```javascript
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`:

```javascript
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` :

```javascript
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**

```xml
<template>
  <h1>
    Welcome to Home Page
  </h1>
</template>
```

**Admin.vue**

```xml
<template>
  <h1>
    Admin Dashboard For Admins Only
  </h1>
</template>
```

**Editor.vue**

```xml
<template>
  <h1>
    Editor Page - Editors and Admins
  </h1>
</template>
```

**NotAuthorized.vue**

```xml
<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**

```xml
<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:

```javascript
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**

```xml
<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 &lt;HasRole /&gt; Wrapper

Let’s not repeat ourselves. Here’s a wrapper for role-only content:

**components/HasRole.vue:**

```xml
<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:**

```xml
<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.
