Skip to content

Permission Groups

A permission group bundles multiple permission strings under a single name. Instead of assigning ten permissions to every editor one by one, you create a group once and assign the group.

Groups are checked automatically by permission.can(), canAny(), and canAll() — no extra work required.

Creating groups

ts
await permission.groups.create('content-managers', [
  'view-posts',
  'edit-posts',
  'publish-posts',
  'delete-posts',
])

Calling groups.create() with a name that already exists replaces the permission list.

ts
// Update the group to add a new permission
await permission.groups.create('content-managers', [
  'view-posts',
  'edit-posts',
  'publish-posts',
  'delete-posts',
  'manage-comments', // added
])

Assigning groups to users

ts
await permission.groups.assign(userId, 'content-managers')

WARNING

If the group does not exist, groups.assign() throws a PermissionError with code NOT_FOUND. Always create the group first.

ts
import { PermissionError } from '@actinode/express-permission'

try {
  await permission.groups.assign(userId, 'ghost-group')
} catch (err) {
  if (err instanceof PermissionError) {
    console.log(err.code)    // 'NOT_FOUND'
    console.log(err.message) // 'Permission group "ghost-group" not found'
  }
}

Getting all groups for a user

ts
const groups = await permission.get.groups(userId)
// ['content-managers', 'beta-testers']

How group checks work

When any check runs — can, canAny, canAll — it expands all groups the user belongs to and includes their permissions in the check. You never need to call group-specific check methods.

ts
// Assuming 'content-managers' includes 'publish-posts':
await permission.check.can(userId, 'publish-posts') // true

// Middleware also picks it up automatically:
app.post('/posts/:id/publish',
  permission.can('publish-posts'),
  handler,
)

Real-world example: content platform

A content platform typically has several distinct roles with overlapping permissions. Groups let you define capability sets once and reuse them.

ts
// Define groups during app startup / seeding
await permission.groups.create('readers', [
  'view-posts',
  'view-comments',
])

await permission.groups.create('writers', [
  'view-posts',
  'view-comments',
  'create-posts',
  'edit-own-posts',
])

await permission.groups.create('content-managers', [
  'view-posts',
  'view-comments',
  'create-posts',
  'edit-own-posts',
  'edit-any-post',
  'delete-posts',
  'publish-posts',
  'manage-comments',
])

await permission.groups.create('admins', [
  'view-posts',
  'view-comments',
  'create-posts',
  'edit-own-posts',
  'edit-any-post',
  'delete-posts',
  'publish-posts',
  'manage-comments',
  'manage-users',
  'manage-roles',
  'view-analytics',
])

// Assign groups at user creation or promotion
await permission.groups.assign(newUserId, 'readers')
await permission.groups.assign(writerUserId, 'writers')
await permission.groups.assign(editorUserId, 'content-managers')
await permission.groups.assign(adminUserId, 'admins')

Routes then stay clean and declarative:

ts
app.get('/posts',
  permission.can('view-posts'),
  listPostsHandler,
)

app.post('/posts',
  permission.can('create-posts'),
  createPostHandler,
)

app.put('/posts/:id',
  permission.canAny(['edit-own-posts', 'edit-any-post']),
  updatePostHandler,
)

app.delete('/posts/:id',
  permission.can('delete-posts'),
  deletePostHandler,
)

app.get('/admin/users',
  permission.can('manage-users'),
  listUsersHandler,
)

Combining groups with direct permissions

Users can hold both group assignments and direct permissions at the same time. All three vectors are checked together.

ts
// Give a contractor writer access plus one extra permission
await permission.groups.assign(contractorId, 'writers')
await permission.assign.permission(contractorId, 'upload-media')

Groups vs Roles

GroupsRoles
What it storesA list of permission stringsA named label; permissions linked in the DB
How permissions are definedInline string arrayVia DB relation (Role ↔ Permission)
Best forBundling permissions for bulk assignmentSemantic labels (admin, editor) that carry permissions
Can be used togetherYesYes

Need an admin panel?

Get a beautiful admin UI for all actinode packages. Contact us to learn more about our premium admin package.

No spam, ever. Unsubscribe at any time.

Released under the MIT License.