Nuxt Authorization: How to Implement Team Role-Based Access Control in Nuxt 3
Tanay KarnikNovember 27th, 2024

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

Client-Side Authorization

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

~/plugins/authorization-resolver.ts
export default defineNuxtPlugin({
  name: "authorization-resolver",
  parallel: true,
  setup() {
    return {
      provide: {
        authorization: {
          resolveClientUser: () => useAuth().data.value?.user,
        },
      },
    };
  },
});

Server-Side Authorization

Similarly for the server:

~/server/plugins/authorization-resolver.ts
import { getServerSession } from "#auth";

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

Defining Type-Safe Abilities

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

~/shared/utils/abilities.ts
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),
);

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
  // [...]
}

Using Abilities in Components

Check permissions in your Vue components:

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

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",
          });
        }
      },
    },
  });
});

Use it in your API routes:

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

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.

Saas-Boilerplate.dev

The Nuxt SaaS boilerplate you've been waiting for.

Copyright 2025 Saas-Boilerplate.dev.