A modern, signal-based Angular library for managing permissions and roles with full TypeScript support.
- 🚦 Signal-based reactivity - Built on Angular Signals for optimal performance
- 🎯 Clean Control Flow - Works seamlessly with
@if,@for,@switch - 🛡️ Route Guards - Functional guards with async loading support
- 📦 Standalone - No modules required, tree-shakable
- 🔌 Policies - Define complex permission rules as reusable policies
- 🌳 Wildcards - Hierarchical permission matching (
admin:*) - 🧪 Testing Utilities - Easy mocking for unit tests
- 📱 Angular 17+ - Modern Angular features support
npm install ngx-signal-permissions// app.config.ts
import { ApplicationConfig } from '@angular/core';
import {
providePermissions,
withPermissions,
withRoles,
} from 'ngx-signal-permissions';
export const appConfig: ApplicationConfig = {
providers: [
providePermissions(
withPermissions(['posts:read', 'posts:write']),
withRoles({
admin: ['users:manage', 'settings:edit'],
editor: ['posts:publish'],
})
),
],
};import { Component } from '@angular/core';
import { hasPermission, hasRole } from 'ngx-signal-permissions';
@Component({
selector: 'app-dashboard',
template: `
@if (canEdit()) {
<button>Edit Post</button>
}
@if (isAdmin()) {
<admin-panel />
}
`,
})
export class DashboardComponent {
canEdit = hasPermission('posts:write');
isAdmin = hasRole('admin');
}The PermissionsStore is the central service that manages all permissions and roles:
import { inject } from '@angular/core';
import { PermissionsStore } from 'ngx-signal-permissions';
export class MyService {
private store = inject(PermissionsStore);
loadUserPermissions(user: User) {
this.store.loadPermissions(user.permissions);
this.store.loadRoles(user.roles);
}
checkAccess() {
// Immediate value
if (this.store.hasPermission('admin')) {
// ...
}
// Reactive signal
const canEdit = this.store.hasPermissionSignal('posts:edit');
}
}The cleanest way to use permissions with Angular's control flow:
import {
hasPermission,
hasAnyPermission,
hasAllPermissions,
hasRole,
hasAnyRole,
authorize,
withPermissions,
} from 'ngx-signal-permissions';
@Component({
template: `
@if (canRead()) { <content /> }
@if (canManage()) { <admin-tools /> }
@if (isPrivileged()) { <premium-content /> }
`,
})
export class MyComponent {
// Single permission
canRead = hasPermission('posts:read');
// Multiple permissions (any)
canManage = hasAnyPermission(['posts:edit', 'posts:delete']);
// Multiple permissions (all required)
needsAll = hasAllPermissions(['read', 'write', 'delete']);
// Role check
isAdmin = hasRole('admin');
// Combined check
isPrivileged = authorize({
permissions: ['premium:access'],
roles: ['subscriber', 'admin'],
strategy: 'any',
});
// Composable pattern
protected perms = withPermissions();
// Then use: this.perms.can('edit'), this.perms.isRole('admin'), etc.
}For template-based permission checks:
import {
HasPermissionDirective,
HasRoleDirective,
ExceptPermissionDirective,
} from 'ngx-signal-permissions';
@Component({
imports: [HasPermissionDirective, HasRoleDirective, ExceptPermissionDirective],
template: `
<!-- Single permission -->
<button *hasPermission="'delete'">Delete</button>
<!-- Multiple permissions -->
<nav *hasPermission="['admin', 'moderator']; strategy: 'any'">
Admin Nav
</nav>
<!-- With else template -->
<div *hasPermission="'premium'; else freeTpl">
Premium Content
</div>
<ng-template #freeTpl>
<button>Upgrade</button>
</ng-template>
<!-- Except directive -->
<div *exceptPermission="'guest'">
Logged in content
</div>
<!-- Role directive -->
<admin-panel *hasRole="'admin'" />
`,
})
export class MyComponent {}Declarative permission blocks:
import { WhenPermittedComponent } from 'ngx-signal-permissions';
@Component({
imports: [WhenPermittedComponent],
template: `
<when-permitted only="admin">
<admin-dashboard />
</when-permitted>
<when-permitted [only]="['edit', 'delete']" strategy="all">
<editor-toolbar />
<ng-template fallback>
<read-only-view />
</ng-template>
</when-permitted>
<when-permitted role="premium">
<premium-features />
</when-permitted>
`,
})
export class MyComponent {}Protect routes with functional guards:
import {
permissionGuard,
permissionMatcher,
routePermissionGuard,
} from 'ngx-signal-permissions';
export const routes: Routes = [
// Inline configuration
{
path: 'admin',
loadComponent: () => import('./admin/admin.component'),
canActivate: [
permissionGuard({
only: 'admin',
redirectTo: '/unauthorized',
}),
],
},
// Multiple permissions
{
path: 'editor',
loadComponent: () => import('./editor/editor.component'),
canActivate: [
permissionGuard({
only: ['posts:write', 'posts:publish'],
strategy: 'all',
redirectTo: '/dashboard',
}),
],
},
// Prevent lazy loading if no permission
{
path: 'premium',
loadChildren: () => import('./premium/routes'),
canMatch: [permissionMatcher({ only: 'premium' })],
},
// Route data based
{
path: 'settings',
loadComponent: () => import('./settings/settings.component'),
canActivate: [routePermissionGuard],
data: {
permissions: {
only: ['settings:view'],
except: ['banned'],
redirectTo: '/home',
},
},
},
];For inline template checks:
import { HasPermissionPipe, HasRolePipe } from 'ngx-signal-permissions';
@Component({
imports: [HasPermissionPipe, HasRolePipe],
template: `
<button [disabled]="!('edit' | hasPermission)">Edit</button>
<button [class.hidden]="!('admin' | hasRole)">Admin</button>
`,
})
export class MyComponent {}Define complex permission rules:
import { PolicyBuilder, PolicyService } from 'ngx-signal-permissions';
// Define policies
const canManageUsers = new PolicyBuilder()
.hasPermission('users:read')
.hasAnyPermission(['users:write', 'users:admin'])
.hasAnyRole(['admin', 'hr'])
.build('canManageUsers');
const canPublishPosts = new PolicyBuilder()
.hasAllPermissions(['posts:write', 'posts:publish'])
.lacksRole('suspended')
.build('canPublishPosts');
// Register in config
providePermissions(
withPolicies([canManageUsers, canPublishPosts])
);
// Use in component
@Component({
template: `
@if (canManage()) {
<user-management />
}
`,
})
export class MyComponent {
private policyService = inject(PolicyService);
canManage = this.policyService.checkSignal('canManageUsers');
}Support for hierarchical permission matching:
// Grant wildcard permission
store.loadPermissions(['admin:*']);
// These all return true
store.hasPermission('admin:read');
store.hasPermission('admin:write');
store.hasPermission('admin:users:delete');
// Configure separator
store.configure({ hierarchySeparator: '.' });
store.loadPermissions(['admin.*']);
store.hasPermission('admin.users.read'); // trueLoad permissions from an API:
// Simple async loading
await store.loadPermissionsAsync(async () => {
const response = await fetch('/api/user/permissions');
return response.json();
});
// Full source configuration
await store.loadFromSource({
loader: () => fetch('/api/user').then(r => r.json()),
mapper: (user) => ({
permissions: user.permissions,
roles: user.roles.reduce((acc, role) => {
acc[role.name] = role.permissions;
return acc;
}, {}),
}),
});
// With provider
providePermissions(
withAsyncSource({
loader: () => fetch('/api/auth').then(r => r.json()),
mapper: (data) => ({
permissions: data.permissions,
roles: data.roles,
}),
})
);import { TestBed } from '@angular/core/testing';
import {
providePermissionsTesting,
createTestStore,
} from 'ngx-signal-permissions/testing';
describe('MyComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [MyComponent],
providers: [
providePermissionsTesting({
permissions: ['read', 'write'],
roles: { admin: ['delete'] },
}),
],
});
});
it('should show edit button for users with write permission', () => {
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('.edit-btn');
expect(button).toBeTruthy();
});
});
// Manual store for isolated tests
it('should check permissions', () => {
const store = createTestStore({
permissions: ['read'],
roles: { user: [] },
});
expect(store.hasPermission('read')).toBeTrue();
expect(store.hasPermission('write')).toBeFalse();
});| Method | Description |
|---|---|
loadPermissions(permissions: string[]) |
Replace all permissions |
addPermission(permission: string) |
Add single permission |
removePermission(permission: string) |
Remove single permission |
hasPermission(permission: string): boolean |
Check permission (immediate) |
hasPermissionSignal(permission: string): Signal<boolean> |
Check permission (reactive) |
hasAnyPermission(permissions: string[]): boolean |
Check any permission |
hasAllPermissions(permissions: string[]): boolean |
Check all permissions |
addRole(role: string, permissions?: string[]) |
Add role |
removeRole(role: string) |
Remove role |
hasRole(role: string): boolean |
Check role (immediate) |
hasRoleSignal(role: string): Signal<boolean> |
Check role (reactive) |
flush() |
Clear all data |
loadFromSource(source: PermissionSource) |
Load from async source |
| Function | Description |
|---|---|
hasPermission(permission) |
Returns Signal<boolean> |
hasAnyPermission(permissions) |
Returns Signal<boolean> |
hasAllPermissions(permissions) |
Returns Signal<boolean> |
hasRole(role) |
Returns Signal<boolean> |
hasAnyRole(roles) |
Returns Signal<boolean> |
authorize(config) |
Combined check, returns Signal<boolean> |
withPermissions() |
Returns composable with all helpers |
| Guard | Description |
|---|---|
permissionGuard(config) |
Inline config guard |
permissionMatcher(config) |
Route matcher |
routePermissionGuard |
Route data based guard |
roleGuard(config) |
Role-only guard |
// Before (ngx-permissions)
<div *ngxPermissionsOnly="['admin']">Admin content</div>
// After (ngx-signal-permissions)
<div *hasPermission="'admin'">Admin content</div>
// Or with modern control flow (recommended)
@if (isAdmin()) {
<div>Admin content</div>
}
// In component
isAdmin = hasRole('admin');MIT