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
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.
// 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
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.
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
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.
// 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.
// 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:
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.
// 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
| Groups | Roles | |
|---|---|---|
| What it stores | A list of permission strings | A named label; permissions linked in the DB |
| How permissions are defined | Inline string array | Via DB relation (Role ↔ Permission) |
| Best for | Bundling permissions for bulk assignment | Semantic labels (admin, editor) that carry permissions |
| Can be used together | Yes | Yes |
