Bouncer integration

This guide covers how the permissions package integrates with AdonisJS Bouncer. You will learn how to:

  • Register generated abilities with Bouncer
  • Authorize actions in controllers
  • Scope permissions for API tokens
  • Combine generated abilities with hand-written Bouncer abilities and policies

Overview

AdonisJS Bouncer authorizes actions through abilities and policies registered on ctx.bouncer. The permissions package bridges into this system by generating a Bouncer ability for each active permission key. You register these generated abilities in your Bouncer middleware, and from that point on they work like any other Bouncer ability.

The flow looks like this:

  1. permissions.abilities() generates one Bouncer ability per active permission key.
  2. You spread them into the Bouncer constructor in your initialize_bouncer_middleware.ts.
  3. In controllers, you call bouncer.authorize('product.create') like any other Bouncer ability.

Behind the scenes, each generated ability loads the user's permissions (from roles, direct assignments, or both), resolves aliases, filters inactive keys, and checks whether the requested key is present.

Registering abilities

Spread the generated abilities into the Bouncer constructor alongside your existing abilities and policies.

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. The result is cached, so repeated calls return the same object.

Each active permission key (for example product.create, billing.refund) becomes a Bouncer ability. Inactive permissions are excluded.

Authorizing in controllers

Use bouncer.authorize to check a permission and throw an AuthorizationException (403) if the user does not have it. Use bouncer.allows or bouncer.denies when you need a boolean instead.

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 update({ bouncer, params }: HttpContext) {
    await bouncer.authorize('product.update')

    // User has permission, proceed
  }
}
app/controllers/billing_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class BillingController {
  async index({ bouncer, view }: HttpContext) {
    const canRefund = await bouncer.allows('billing.refund')

    return view.render('billing/index', { canRefund })
  }
}

Token scoping

When using API tokens, you may want to restrict what a token can do to a subset of the user's permissions. The generated abilities accept an optional second argument: an array of permission keys representing the token's allowed scope.

// Without scoping. Checks against all user permissions
await bouncer.allows('product.create')

// With scoping. Checks the intersection of user permissions and token scope
await bouncer.allows('product.create', ['product.create', 'product.update'])

In the scoped example, even if the user has billing.refund through their roles, the check only considers product.create and product.update because those are the token's allowed abilities. When no second argument is provided, the user's full permission set applies.

Using with access tokens

If you are using AdonisJS Auth access tokens, pass the token's abilities as the second argument when authorizing.

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

export default class ProductsController {
  async store({ auth, bouncer }: HttpContext) {
    const user = auth.getUserOrFail()
    const tokenAbilities = user.currentAccessToken?.abilities

    await bouncer.authorize('product.create', tokenAbilities)
  }
}

When a request uses session authentication (no token), currentAccessToken is undefined and tokenAbilities is undefined, so the full permission set applies. When a request uses an access token, the permissions are narrowed to only those the token was issued with.

Beyond static permissions

The permissions package handles RBAC with a static set of permissions: "editors can create products", "admins can issue refunds". Every user with the right permission can perform the action regardless of which specific resource they are acting on.

Some authorization checks cannot be expressed as static permissions:

  • Resource ownership — "Users can only edit their own posts"
  • Conditional access — "Free-tier users cannot export reports"
  • Relationship-based — "Only the project lead can archive a project"

For these, write Bouncer abilities or policies directly. They coexist with the generated permission abilities in the same Bouncer constructor.

app/abilities/main.ts
import { Bouncer } from '@adonisjs/bouncer'
import Post from '#models/post'
import User from '#models/user'

export const editPost = Bouncer.ability((user: User, post: Post) => {
  return user.id === post.authorId
})

Both kinds of abilities are available on ctx.bouncer.

app/controllers/posts_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import Post from '#models/post'

export default class PostsController {
  async update({ bouncer, params }: HttpContext) {
    const post = await Post.findOrFail(params.id)

    // Static permission check
    await bouncer.authorize('product.update')

    // Resource ownership check
    await bouncer.authorize('editPost', post)
  }
}

Use the permissions package for "can this user perform this type of action" checks. Use Bouncer abilities and policies for "can this user do this to this specific resource" checks.

Terms & License Agreement