Quick start

This guide walks you through setting up the permissions package from scratch. By the end, you will have:

  • A set of permission definitions
  • A Role model that stores permissions
  • A User model with role assignments
  • Authorization checks in a controller using Bouncer

Install and configure

The permissions package uses AdonisJS Bouncer under the hood to authorize actions. If you have not already installed Bouncer, run the following command first.

node ace add @adonisjs/bouncer

This command installs the package, creates the app/abilities/main.ts file, generates the Bouncer middleware at app/middleware/initialize_bouncer_middleware.ts, and registers it in your HTTP kernel.

Next, install and configure the @adonisplus/permissions package.

npm i @adonisplus/permissions
node ace configure @adonisplus/permissions
Steps performed by the configure command
  1. Creates the start/permissions.ts file where you define your application's permissions.
  2. Creates the app/middleware/authorize_middleware.ts middleware for route-level permission checks.
  3. Registers the middleware inside the start/kernel.ts file.
  4. Updates the adonisrc.ts file to register the package's service provider.
  5. Creates the inertia/utils/permissions.tsx file with frontend helpers (only in React or Vue projects).

Define permissions

Permissions represent the actions a user can perform in your application. You define them in a single file so that every part of your codebase references the same set of keys.

The configure command already created the start/permissions.ts file for you. Open it and declare your permissions using the definePermissions function. Each permission follows the resource.action pattern.

start/permissions.ts
import { definePermissions } from '@adonisplus/permissions'

export const permissions = definePermissions({
  product: {
    create: 'Create new products',
    update: 'Update existing products',
    delete: 'Delete products permanently',
  },
  billing: {
    refund: 'Issue refunds to customers',
  },
})

This produces four permission keys: product.create, product.update, product.delete, and billing.refund. All keys are type-safe and autocompleted by your editor.

Create the database tables

You need two database tables to store roles and their assignments:

  • roles: Stores role names and the permissions assigned to each role as a JSON column.
  • user_roles: A pivot table that links users to their roles.
  1. Create the migrations

    Run the following commands to generate the two migration files.

    node ace make:migration roles
    node ace make:migration user_roles
  2. Define the roles table

    Open the generated roles migration file and define the table schema. The permissions column stores a JSON array of permission keys assigned to the role.

    database/migrations/xxxx_create_roles.ts
    import { BaseSchema } from '@adonisjs/lucid/schema'
    
    export default class extends BaseSchema {
      protected tableName = 'roles'
    
      async up() {
        this.schema.createTable(this.tableName, (table) => {
          table.increments('id')
          table.string('name').unique().notNullable()
          table.text('permissions').defaultTo('[]')
          table.timestamp('created_at')
          table.timestamp('updated_at')
        })
      }
    
      async down() {
        this.schema.dropTable(this.tableName)
      }
    }
  3. Define the user_roles pivot table

    Open the generated user_roles migration file and define the pivot table schema. The unique constraint on user_id and role_id prevents duplicate role assignments.

    database/migrations/xxxx_create_user_roles.ts
    import { BaseSchema } from '@adonisjs/lucid/schema'
    
    export default class extends BaseSchema {
      protected tableName = 'user_roles'
    
      async up() {
        this.schema.createTable(this.tableName, (table) => {
          table.increments('id')
          table.integer('user_id').notNullable().unsigned()
          table.integer('role_id').notNullable().unsigned()
          table.unique(['user_id', 'role_id'])
        })
      }
    
      async down() {
        this.schema.dropTable(this.tableName)
      }
    }
  4. Run the migrations

    node ace migration:run

Set up models

The permissions package provides two mixins that add role and permission management to your Lucid models.

Apply withPermissions to the Role model

The withPermissions mixin teaches the Role model how to read and write the permissions JSON column. It adds methods like givePermissions, syncPermissions, and revokePermissions to role instances.

app/models/role.ts
import { RoleSchema } from '#database/schema'
import { compose } from '@adonisjs/core/helpers'
import { withPermissions } from '@adonisplus/permissions'

export default class Role extends compose(
  RoleSchema,
  withPermissions() 
) {}

Apply withRoles to the User model

The withRoles mixin connects users to roles through the pivot table. It adds methods like assignRole, syncRoles, and hasPermission to user instances. You must tell the mixin which model represents roles and which table stores the assignments.

app/models/user.ts
import { UserSchema } from '#database/schema'
import { compose } from '@adonisjs/core/helpers'
import { withRoles } from '@adonisplus/permissions'
import Role from '#models/role'

export default class User extends compose(
  UserSchema,
  withRoles({
    roleModel: () => Role,
    pivotTable: 'user_roles',
  })
) {}

Seed roles

With the models in place, you can create roles and assign permissions to them. Use permissions.getKey() to reference permission keys with full type safety.

commands/seed_roles.ts
import { permissions } from '#start/permissions'
import Role from '#models/role'

const editor = await Role.create({ name: 'editor' })
await editor.givePermissions([
  permissions.getKey('product.create'),
  permissions.getKey('product.update'),
])

const admin = await Role.create({ name: 'admin' })
await admin.syncPermissions(permissions.active())

The givePermissions method adds specific permissions to a role. The syncPermissions method replaces all existing permissions with the provided list. Calling permissions.active() returns every permission key you defined, so the admin role will always have full access.

Assign roles to users

Once roles exist, you can assign them to users. The assignRole method inserts a row into the user_roles pivot table.

const user = await User.findOrFail(1)
await user.assignRole(editor)

A user can hold multiple roles. Their effective permissions are the union of all permissions from every assigned role.

Register abilities with Bouncer

The permissions package integrates with Bouncer by converting your permission definitions into Bouncer abilities. This lets you use the same bouncer.authorize API for both hand-written abilities and role-based permissions.

Open your Bouncer middleware and spread the generated abilities into the Bouncer constructor alongside your existing abilities.

app/middleware/initialize_bouncer_middleware.ts
import { Bouncer } from '@adonisjs/bouncer'
import * as abilities from '#abilities/main'
import { policies } from '#generated/policies'
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

import { permissions } from '#start/permissions'

const allAbilities = {
  ...abilities,
  ...permissions.abilities(),
}

export default class InitializeBouncerMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    ctx.bouncer = new Bouncer(
      () => ctx.auth.user || null,
      abilities,
      allAbilities
      policies
    ).setContainerResolver(ctx.containerResolver)

    return next()
  }
}

declare module '@adonisjs/core/http' {
  export interface HttpContext {
    bouncer: Bouncer<
      Exclude<HttpContext['auth']['user'], undefined>,
      typeof abilities,
      typeof allAbilities,
      typeof policies
    >
  }
}

The permissions.abilities() call is placed outside the middleware class so it runs once at import time, not on every request.

Authorize in a controller

You can now protect controller actions using bouncer.authorize. Pass the permission key as a string and Bouncer will check whether the authenticated user holds a role with that permission. If the check fails, Bouncer throws a 403 error automatically.

app/controllers/products_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class ProductsController {
  async store({ bouncer }: HttpContext) {
    await bouncer.authorize('product.create')

    // User has permission, proceed
  }

  async destroy({ bouncer, params }: HttpContext) {
    await bouncer.authorize('product.delete')

    // User has permission, proceed
  }
}

With the roles from the seed step, a user with the "editor" role can create products but receives a 403 when trying to delete them. A user with the "admin" role can do both because the admin role holds every permission.

What's next

Terms & License Agreement