Skip to main content

Overview

BunShip enforces access control through a role-based permission system. Every organization member has a role, and each role maps to a set of permissions. Middleware checks these permissions before a route handler executes, so unauthorized requests never reach your business logic.

Role Hierarchy

Four built-in roles are ordered from most to least privileged:

Permission Definitions

Every permission follows the pattern resource:action. A wildcard suffix (resource:*) grants all actions for that resource. The global wildcard (*) grants every permission.

Role-Permission Matrix

The mapping from roles to permissions is defined in featuresConfig.organizations.permissions:
The full matrix, expanded:

How Permissions Are Checked

BunShip provides two middleware functions for access control: requirePermission() for permission-based checks and requireRole() for role-based checks.

Permission Check

requirePermission() reads the user’s role from their membership, looks up the role’s permission list, and runs it through the hasPermission() function:

Permission Resolution Logic

The hasPermission() function checks three levels of matching:
This means the owner role (with ["*"]) passes every check, and admin’s "members:*" grants "members:read", "members:invite", "members:update", and "members:remove" in a single entry.

Role Check

For cases where you need to restrict by role name rather than permission, use requireRole():
BunShip exports two convenience shortcuts:

Using Permission Middleware in Routes

A typical organization-scoped route chains the middleware in order:

Adding Custom Roles

To add a new role (for example, billing-admin with access to billing and org settings):
1

Add the role to the features config

2

Update the database enum

Add the new role to the membership and invitation schema enums:
3

Generate and run a migration

No middleware changes are needed. The existing requirePermission() and requireRole() functions will pick up the new role automatically because they read from featuresConfig at runtime.

Adding Custom Permissions

To protect a new resource type (for example, reports):
1

Define the permissions

2

Assign permissions to roles

3

Use in routes

TypeScript will autocomplete the new permission strings since Permission is derived from the permissions object keys.

API Key Scopes

API keys use a separate scope system from RBAC permissions. Scopes follow a action:resource format (note the reversed order compared to RBAC permissions):

How Scopes Relate to Permissions

When an API key is used for authentication, the system checks both that the key is valid and that its scopes cover the requested operation. A key with ["read:projects"] can list projects but cannot create or delete them.
API key scopes are deliberately more coarse-grained than RBAC permissions. A write:projects scope grants create, update, and delete in one entry, while RBAC separates these into individual permissions. This keeps key creation simple for integrators.

Utility Functions

The @bunship/config package exports several helper functions for working with permissions programmatically:
These functions are useful when building UI elements that show or hide features based on the current user’s permissions, or when writing service-layer authorization checks outside of middleware.