Logo

dev-resources.site

for different kinds of informations.

Nuxt Authorization: How to Implement Team Role-Based Access Control in Nuxt 3

Published at
12/4/2024
Categories
nuxt
typescript
prisma
authjs
Author
tanay
Categories
4 categories in total
nuxt
open
typescript
open
prisma
open
authjs
open
Author
5 person written this
tanay
open
Nuxt Authorization: How to Implement Team Role-Based Access Control in Nuxt 3

If you're building a multi-tenant SaaS in Nuxt 3, you'll need a robust permissions system.
Here's how I built a type-safe RBAC system that scales from small teams to enterprise, using Prisma and tRPC.

The Stack

Basic Setup

First, install the authorization module:

pnpx nuxi@latest module add nuxt-authorization
Enter fullscreen mode Exit fullscreen mode

Client-Side Authorization

Set up a plugin to resolve the user on the client:

export default defineNuxtPlugin({
  name: "authorization-resolver",
  parallel: true,
  setup() {
    return {
      provide: {
        authorization: {
          resolveClientUser: () => useAuth().data.value?.user,
        },
      },
    };
  },
});
Enter fullscreen mode Exit fullscreen mode

Server-Side Authorization

Similarly for the server:

import { getServerSession } from "#auth";

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook("request", async (event) => {
    event.context.$authorization = {
      resolveServerUser: async () => {
        return (await getServerSession(event))?.user;
      },
    };
  });
});
Enter fullscreen mode Exit fullscreen mode

Defining Type-Safe Abilities

Here's how we define shared abilities that work on both client and server:

interface User {
  id: string;
  teams?: string[];
  permissions?: Record<string, string[]>;
}

const hasTeamPermission = (
  user: User | null,
  teamId: string,
  permission: string,
): boolean =>
  !!user?.teams?.includes(teamId) &&
  (user?.permissions?.[teamId] || []).includes(permission);

export const listTeams = defineAbility(() => true);

export const getTeamDetails = defineAbility(
  (user: User, teamId: string) => !!(teamId && user?.teams?.includes(teamId)),
);

export const updateTeamDetails = defineAbility(
  (user: User | null, teamId: string) =>
    hasTeamPermission(user, teamId, PERMISSIONS.TEAMS.UPDATE),
);
Enter fullscreen mode Exit fullscreen mode

Database Schema

Your Prisma schema needs to support roles and permissions:

model TeamMembership {
  id     String @id @default(cuid())
  role Role @relation(fields: [roleId], references: [id])
  // [...]
}

model Role {
  id           String  @id @default(cuid())
  teamId       String?
  name         String
  description  String?
  isDefault    Boolean @default(false)
  isSystemRole Boolean @default(false)
  permissions Permission[]
  // [...]
}

model Permission {
  id          String  @id @default(cuid())
  title       String
  description String?
  action      String
  roleId      String
  // [...]
}
Enter fullscreen mode Exit fullscreen mode

Using Abilities in Components

Check permissions in your Vue components:

<Can :ability="deleteTeamAbility" :args="[team?.id || '']">
  <!-- Protected content here -->
</Can>
Enter fullscreen mode Exit fullscreen mode

Type-Safe API Authorization

Create a tRPC procedure for checking abilities:

export const abilityProcedure = protectedProcedure.use(async (opts) => {
  const { ctx } = opts;
  return opts.next({
    ctx: {
      ...ctx,
      allows: async function allow<Ability extends BouncerAbility<any>>(
        ability: Ability,
        ...args: BouncerArgs<Ability>
      ) {
        return await allows(ctx.event, ability, ...args);
      },
      authorize: async function auth<Ability extends BouncerAbility<any>>(
        ability: Ability,
        ...args: BouncerArgs<Ability>
      ) {
        try {
          await authorize(ctx.event, ability, ...args);
        } catch (error) {
          throw new TRPCError({
            code: "FORBIDDEN",
            message: error instanceof Error ? error.message : "Not authorized",
          });
        }
      },
    },
  });
});
Enter fullscreen mode Exit fullscreen mode

Use it in your API routes:

{
  get: abilityProcedure
    .input(
      z.object({
        teamIdentifier: z.string(),
      }),
    )
    .query(async ({ ctx: { authorize, user, prisma }, input }) => {
      await authorize(getTeamDetails, team.id);
      // Protected logic here
    }),
}
Enter fullscreen mode Exit fullscreen mode

Why this works well

  • Fully type-safe from database to UI
  • No external authorization service needed
  • Works seamlessly with any auth provider
  • Scales from simple to complex permission structures

Try it yourself

Want to see this RBAC system in action? This exact implementation is part of my Nuxt SaaS boilerplate.

If you're building a multi-tenant SaaS, check it out—it comes with everything you need: type-safe APIs using tRPC, team management, authentication, billing, and more. Every feature is built with the same attention to developer experience as this permissions system.

prisma Article's
30 articles in total
Favicon
How to Fix the “Record to Delete Does Not Exist” Error in Prisma
Favicon
Building Type-Safe APIs: Integrating NestJS with Prisma and TypeScript
Favicon
Deploying an Existing Express API + Prisma + Supabase Project to Vercel
Favicon
Exploring the Power of Full-Stack Development with Next.js and Prisma
Favicon
How to integrate GitHub CopilotKit with Prisma Integration into your nextJs project Using OpenAI
Favicon
วิธีทำ Auth API ด้วย Express, JWT, MySQL และ Prisma
Favicon
Prisma
Favicon
How we built "Space-Ease" using Next.js
Favicon
Query Objects Instead of Repositories: A Modern Approach to Data Access
Favicon
Common Data Loss Scenarios & Solutions in Prisma Schema Changes
Favicon
How I Solved Common Prisma ORM Errors: Debugging Tips and Best Practices
Favicon
Prisma 101 baby.
Favicon
Prisma
Favicon
QueryBuilder in Action Part 1
Favicon
Prisma ORM: Revolutionizing Database Interactions
Favicon
Prisma & MongoDB: server to be run as a replica set
Favicon
Using GenAI to Tackle Complex Prisma Model Migrations
Favicon
When Embedded AuthN Meets Embedded AuthZ - Building Multi-Tenant Apps With Better-Auth and ZenStack
Favicon
Building Multi-Tenant Apps Using StackAuth's "Teams" and Next.js
Favicon
The Most Awaited Prisma Course Is Here! 😍
Favicon
**Building a Full-Stack Next.js Starter Kit: Authentication, GraphQL, and Testing**
Favicon
Integrate DAYTONA and let the magic begin....
Favicon
Cloudflare D1 and Prisma: Not a Good Combination (For Now)
Favicon
Resolving the `DO $$` Issue in Drizzle ORM with Nile Postgres
Favicon
Nuxt Authorization: How to Implement Team Role-Based Access Control in Nuxt 3
Favicon
Getting Started with Prisma, SQLite, and Express
Favicon
Senior Developer Advocate
Favicon
Building Multi-Tenant Apps Using Clerk's "Organization" and Next.js
Favicon
How to use ORMs (Prisma / Drizzle / Knex.js) in a TypeScript backend built with Encore.ts
Favicon
Pagination and Sorting with Prisma in TypeScript

Featured ones: