π Modern, Laravel-inspired permissions system for React/Inertia.js
Advanced pattern matching β’ Boolean expressions β’ Zero dependencies β’ Full TypeScript support
π Documentation β’ π Quick Start β’ π‘ Examples β’ π― Patterns
- π― Laravel-First - Built specifically for Laravel + Inertia.js stack
- β‘ Zero Setup - Works out of the box, no providers needed
- π§ Smart Patterns - Advanced wildcard and boolean logic support
- π¦ Tiny Bundle - Only 17.7kB, tree-shakeable, zero dependencies
- π Type Safe - Full TypeScript support with excellent IntelliSense
- π¨ Modern React - Hooks-first design, no legacy patterns
- β
Laravel Convention - Follows
@can
directive pattern from Laravel Blade - β
Advanced Pattern Matching - Wildcards (
*
), single chars (?
), complex expressions - β
Boolean Logic Support - Boolean values and logical operators (
||
,&&
,|
,&
) - β Custom Permissions - Override auth permissions with custom arrays
- β Expression Validation - Safe evaluation of complex permission expressions
- β TypeScript Support - Full type safety with excellent IntelliSense
- β Spatie Integration - Seamless integration with Laravel Spatie Permission
- β Performance Optimized - Efficient pattern matching and memoization
- β Zero Dependencies - Only peer dependencies on React and Inertia.js
npm install @devwizard/laravel-react-permissions
# or
yarn add @devwizard/laravel-react-permissions
# or
pnpm add @devwizard/laravel-react-permissions
Add permissions to your Inertia middleware:
// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user() ? [
'id' => $request->user()->id,
'name' => $request->user()->name,
'email' => $request->user()->email,
'permissions' => $request->user()->getAllPermissions()->pluck('name')->toArray(),
] : null,
],
];
}
import { Can, usePermissions } from '@devwizard/laravel-react-permissions';
// Basic usage
function UserManagement() {
return (
<Can permission="users.create">
<button>Create User</button>
</Can>
);
}
// Advanced patterns
function AdminPanel() {
const { hasPermission } = usePermissions();
const canAccess = hasPermission('admin.* || moderator.users');
return (
<Can permission="(admin.* || moderator.*) && active.user">
<AdminDashboard />
</Can>
);
}
// Custom permissions
function FeatureFlag() {
const featurePermissions = ['feature.beta', 'feature.advanced'];
return (
<Can
permission="feature.beta"
permissions={featurePermissions}
fallback={<div>Feature not available</div>}
>
<BetaFeature />
</Can>
);
}
React component for conditional rendering based on permissions:
<Can permission="users.* || admin.access" fallback={<div>Access denied</div>}>
<UserManagement />
</Can>
Props:
permission?: string | boolean
- Permission to check (supports patterns and boolean logic)permissions?: string[]
- Custom permissions array (overrides auth permissions)fallback?: ReactNode
- What to render when access is deniedchildren: ReactNode
- Content to render when access is granted
Hook for programmatic permission checking:
const { hasPermission, permissions } = usePermissions();
const canEdit = hasPermission('posts.edit || posts.moderate');
// With custom permissions
const { hasPermission } = usePermissions(['custom.perm', 'another.perm']);
Parameters:
permissions?: string[]
- Optional custom permissions array
Returns:
hasPermission: (permission: string | boolean) => boolean
- Check permission functionpermissions: string[]
- Current permissions arrayhasAnyPermission: (permissions: string[]) => boolean
- Check any of multiple permissionshasAllPermissions: (permissions: string[]) => boolean
- Check all permissionsisAuthenticated: boolean
- Authentication status
Higher-order component for wrapping components:
const ProtectedComponent = withPermission(MyComponent, 'admin.access', <Unauthorized />);
// With custom permissions
const ProtectedFeature = withPermission(
FeatureComponent,
'feature.beta',
<div>Feature not available</div>,
['feature.beta', 'admin.override']
);
<Can permission="users.*"> {/* users.create, users.edit, users.delete */}
<Can permission="admin.*"> {/* admin.users, admin.settings, admin.logs */}
<Can permission="*.create"> {/* users.create, posts.create, etc. */}
<Can permission="user?.edit"> {/* user1.edit, user2.edit, userA.edit */}
<Can permission="level?.access"> {/* level1.access, level2.access, etc. */}
<Can permission="users.edit || posts.edit"> {/* OR logic */}
<Can permission="admin.access && users.manage"> {/* AND logic */}
<Can permission="(users.* || posts.*) && active"> {/* Grouped expressions */}
<Can permission="true"> {/* Always allow */}
<Can permission="false"> {/* Always deny */}
{
/* Multiple operators */
}
<Can permission="(admin.* && active) || (moderator.* && verified)">
<SensitiveData />
</Can>;
{
/* Pattern combinations */
}
<Can permission="*.create || admin.* || super.user">
<CreateButton />
</Can>;
{
/* Business logic */
}
<Can permission="(department.hr && level.manager) || admin.override">
<EmployeeData />
</Can>;
function TenantDashboard() {
const [tenantPermissions, setTenantPermissions] = useState([]);
useEffect(() => {
fetchTenantPermissions().then(setTenantPermissions);
}, []);
return (
<Can permission="tenant.admin || tenant.manager" permissions={tenantPermissions}>
<TenantControls />
</Can>
);
}
const featureFlags = ['feature.newUi', 'feature.advancedSearch'];
<Can permission="feature.newUi" permissions={featureFlags}>
<NewUserInterface />
</Can>;
// Mock permissions for testing
const mockPermissions = ['users.view', 'posts.create'];
<Can permission="users.view" permissions={mockPermissions}>
<TestComponent />
</Can>
// No permissions (empty array)
<Can permission="admin.access" permissions={[]}>
<div>Should not render</div>
</Can>
// From localStorage
const savedPermissions = JSON.parse(localStorage.getItem('permissions') || '[]');
// From API response
const apiPermissions = user?.customPermissions || [];
// From configuration
const configPermissions = ['feature.beta', 'admin.access'];
<Can permission="feature.beta" permissions={configPermissions}>
<BetaFeature />
</Can>;
Full TypeScript support with comprehensive type definitions:
import type {
CanProps,
UsePermissionsReturn,
WithPermissionOptions,
} from '@devwizard/laravel-react-permissions';
// Typed component
const MyPermissionComponent: React.FC<CanProps> = ({ permission, permissions, children }) => {
return (
<Can permission={permission} permissions={permissions}>
{children}
</Can>
);
};
// Typed hook usage
const PermissionChecker: React.FC = () => {
const { hasPermission, permissions, isAuthenticated }: UsePermissionsReturn = usePermissions();
return <div>{isAuthenticated && hasPermission('users.view') && <UserList />}</div>;
};
- React: 18.x, 19.x
- Inertia.js: 1.x, 2.x
- TypeScript: 5.x+
- Node.js: 16.x+
- Zero Runtime Dependencies - Only peer dependencies
- Tree Shakeable - Import only what you need
- Optimized Pattern Matching - Efficient regex compilation
- Memoized Results - Cached permission evaluations
- Small Bundle Size - Minimal overhead
- Safe Expression Evaluation - Protected against code injection
- Input Validation - Syntax checking for complex expressions
- No Eval Usage - Uses Function constructor safely
- Permission Isolation - Custom permissions don't affect auth state
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
MIT License - see the LICENSE file for details.
IQBAL HASAN
- GitHub: @devwizardHq
- Organization: DevWizard
- Laravel team for the excellent
@can
directive inspiration - Spatie for the Laravel Permission package
- Inertia.js team for the seamless React-Laravel integration
Made with β€οΈ by DevWizard
Add permissions to your Inertia middleware:
// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user() ? [
'id' => $request->user()->id,
'name' => $request->user()->name,
'email' => $request->user()->email,
'permissions' => $request->user()->getAllPermissions()->pluck('name')->toArray(),
] : null,
],
];
}
import { Can, usePermissions } from '@devwizard/laravel-react-permissions';
// Basic usage
function UserManagement() {
return (
<Can permission="users.create">
<button>Create User</button>
</Can>
);
}
// Advanced patterns
function AdminPanel() {
const { hasPermission } = usePermissions();
const canAccess = hasPermission('admin.* || moderator.users');
return (
<Can permission="(admin.* || moderator.*) && active.user">
<AdminDashboard />
</Can>
);
}
// Custom permissions
function FeatureFlag() {
const featurePermissions = ['feature.beta', 'feature.advanced'];
return (
<Can
permission="feature.beta"
permissions={featurePermissions}
fallback={<div>Feature not available</div>}
>
<BetaFeature />
</Can>
);
}
The permissions system supports powerful pattern matching and boolean expressions for maximum flexibility:
<Can permission="true"> {/* Always visible to authenticated users */}
<Can permission="false"> {/* Never visible (useful for testing) */}
<Can permission="users.* || posts.*"> {/* User management OR post management */}
<Can permission="admin.access | manager.access"> {/* Admin OR manager access */}
<Can permission="users.* && admin.access"> {/* User permissions AND admin access */}
<Can permission="reports.view & security.clearance"> {/* Reports AND security clearance */}
<Can permission="(users.* || posts.*) && admin.access">
{/* (User OR post management) AND admin access */}
</Can>
<Can expression="(admin.* && security.*) || system.override">
{/* Admin with security OR system override */}
</Can>
Instead of always using auth permissions, you can provide your own permissions array:
// Static permissions array
const staticPerms = ['users.create', 'users.edit', 'posts.view'];
<Can
permission="users.create"
permissions={staticPerms}
>
<CreateUserButton />
</Can>
// Dynamic permissions from API or localStorage
<Can
permission="posts.view || posts.edit"
permissions={getDynamicPermissions()}
>
<PostManagement />
</Can>
// Empty permissions (no access)
<Can
permission="admin.access"
permissions={[]}
fallback={<div>No permissions available</div>}
>
<AdminPanel />
</Can>
const customPerms = ['feature.beta', 'admin.override'];
const { hasPermission } = usePermissions(customPerms);
const hasBeta = hasPermission('feature.beta'); // true
const hasAdmin = hasPermission('admin.access'); // false (not in custom array)
- Feature Flags:
permissions={featureFlags}
- Static Config:
permissions={configPermissions}
- API Response:
permissions={apiPermissions}
- Testing:
permissions={mockPermissions}
orpermissions={[]}
- localStorage:
permissions={JSON.parse(localStorage.getItem('perms'))}
<Can permission="users.*"> {/* matches: users.create, users.edit, users.delete */}
<Can permission="admin.*"> {/* matches: admin.users, admin.settings, admin.reports */}
<Can permission="user?.edit"> {/* matches: user1.edit, user2.edit, userA.edit */}
<Can permission="level?.view"> {/* matches: level1.view, level2.view, level3.view */}
<Can permission="users.create"> {/* exact match only */}
// Boolean with logical operators
<Can permission="users.* && (admin.access || true)">
<div>User management (admin access helps but not required)</div>
</Can>
// Complex multi-level logic
<Can permission="(admin.* && security.*) || (manager.* && reports.view) || system.override">
<div>Multiple access paths to sensitive data</div>
</Can>
// Feature flags with boolean logic
<Can permission="feature.beta || admin.access">
<div>Beta feature or admin access</div>
</Can>
// Negation (coming soon)
<Can permission="admin.access && !feature.disabled">
<div>Admin access unless feature is disabled</div>
</Can>
const { isValidExpression, checkExpression } = usePermissions();
// Validate expressions before using them
const isValid = isValidExpression('users.* && (admin.access || manager.role)');
const result = checkExpression('(admin.* && security.*) || system.override');
- β
Can Component - Conditionally render components (similar to Laravel's
@can
Blade directive) - β usePermissions Hook - Check permissions programmatically
- β withPermission HOC - Wrap components with permission logic
- β Navigation Integration - Permission-aware navigation items
- β
Boolean Logic Support - Boolean values (
true
/false
) and logical operators (||
,&&
,|
,&
) - β
Advanced Pattern Matching - Wildcards (
*
), single chars (?
), complex expressions - β Expression Validation - Syntax checking and safe evaluation of complex expressions
- β Flexible Permission Checking - Single, multiple, all, any, pattern-based, and boolean logic
- β TypeScript Support - Fully typed
- Copy this entire
permissions
folder to yourresources/js/
directory - Update your Laravel middleware to pass permissions via Inertia props
- Import and use the components
Add to your HandleInertiaRequests.php
middleware:
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user() ? [
...$request->user()->toArray(),
'permissions' => $request->user()->getAllPermissions()->pluck('name'),
] : null,
],
];
}
The <Can>
component follows Laravel's @can
convention and supports:
- Pattern matching with wildcards (
*
) and single chars (?
) - Boolean logic (true/false permissions)
- Logical operators (
||
,&&
,|
,&
) - Custom permissions array override
- Fallback rendering for unauthorized access
import { Can } from '@/permissions';
<Can permission="users.create">
<button>Create User</button>
</Can>;
import { usePermissions } from '@/permissions';
function MyComponent() {
const { hasPermission } = usePermissions();
return <div>{hasPermission('users.edit') && <button>Edit User</button>}</div>;
}
import { type NavItem } from '@/permissions';
const navItems: NavItem[] = [
{
title: 'Users',
href: '/users',
permission: 'users.view',
},
{
title: 'Admin',
href: '/admin',
pattern: 'admin.*',
},
];
import { withPermission } from '@/permissions';
const ProtectedComponent = withPermission(MyComponent, {
permission: 'admin.access',
fallback: <div>Access denied</div>,
});
Props:
permission?: string
- Single permission (supports patterns, boolean values, and logical operators)expression?: string
- Complex boolean expression with logical operatorsanyPermissions?: string[]
- User needs ANY of these (each can be a pattern/expression)allPermissions?: string[]
- User needs ALL of these (each can be a pattern/expression)pattern?: string
- Legacy wildcard pattern (usepermission
prop instead)anyPatterns?: string[]
- User needs ANY pattern matchallPatterns?: string[]
- User needs ALL pattern matchesfallback?: ReactNode
- Rendered when permission deniedrequireAuth?: boolean
- Require authentication (default: true)
Returns:
userPermissions: string[]
- User's permissionshasPermission(permission: string): boolean
- Check permission (supports all features)hasAnyPermission(permissions: string[]): boolean
- Check any permissionhasAllPermissions(permissions: string[]): boolean
- Check all permissionshasPermissionPattern(pattern: string): boolean
- Advanced pattern checkinghasAnyPattern(patterns: string[]): boolean
- Check any pattern matcheshasAllPatterns(patterns: string[]): boolean
- Check all patterns matchgetMatchingPermissions(pattern: string): string[]
- Get permissions matching patterncheckExpression(expression: string): boolean
- Evaluate complex boolean expressionsisValidExpression(expression: string): boolean
- Validate expression syntaxisAuthenticated: boolean
- User authentication status
permissions/
βββ index.ts # Main exports
βββ components/
β βββ can.tsx # Main Can component
β βββ with-permission.tsx # HOC wrapper
βββ hooks/
β βββ use-permissions.tsx # Main permissions hook
βββ types/
βββ index.ts # TypeScript definitions
- Copy the entire
permissions
folder - Update import paths if your project structure differs
- Ensure your Laravel backend passes permissions via Inertia props
- Update your existing components to use
<Can>
instead of manual permission checks
- Component names follow Laravel patterns (
Can
β@can
) - Permission patterns match Laravel conventions (
resource.action
) - File structure follows Laravel organizational principles
- TypeScript interfaces mirror Laravel model structures
interface CanProps {
permission: string | boolean; // Permission to check
permissions?: string[]; // Optional custom permissions array
fallback?: React.ReactNode; // Content to render when access denied
children: React.ReactNode; // Content to render when access granted
}
Examples:
// Using auth permissions (default)
<Can permission="users.create">
<CreateButton />
</Can>
// Using custom permissions array
<Can
permission="admin.access"
permissions={['admin.access', 'super.user']}
>
<AdminPanel />
</Can>
// With fallback
<Can
permission="posts.view"
fallback={<div>No access</div>}
>
<PostList />
</Can>
function usePermissions(permissions?: string[]): {
hasPermission: (permission: string | boolean) => boolean;
permissions: string[];
};
Parameters:
permissions
(optional): Array of permissions to use instead of auth permissions
Returns:
hasPermission
: Function to check if user has a specific permissionpermissions
: Current permissions array being used
Examples:
// Using auth permissions
const { hasPermission, permissions } = usePermissions();
// Using custom permissions
const { hasPermission } = usePermissions(['custom.perm', 'another.perm']);
// Check permissions
const canEdit = hasPermission('posts.edit');
const hasAny = hasPermission('admin.* || moderator.*');
function withPermission<T extends object>(
WrappedComponent: React.ComponentType<T>,
requiredPermission: string | boolean,
fallback?: React.ReactNode,
permissions?: string[]
): React.ComponentType<T>;
Parameters:
WrappedComponent
: Component to wraprequiredPermission
: Permission required to render componentfallback
(optional): What to render when access deniedpermissions
(optional): Custom permissions array
Examples:
// Basic HOC usage
const ProtectedUserList = withPermission(UserList, 'users.view', <div>No access to users</div>);
// With custom permissions
const ProtectedFeature = withPermission(
FeatureComponent,
'feature.beta',
<div>Feature not available</div>,
['feature.beta', 'admin.override']
);
<Can permission="users.*"> {/* users.create, users.edit, users.delete */}
<Can permission="admin.*"> {/* admin.users, admin.settings, admin.logs */}
<Can permission="*.create"> {/* users.create, posts.create, etc. */}
<Can permission="user?.edit"> {/* user1.edit, user2.edit, userA.edit */}
<Can permission="level?.access"> {/* level1.access, level2.access, etc. */}
<Can permission="users.edit || posts.edit"> {/* OR logic */}
<Can permission="admin.access && users.manage"> {/* AND logic */}
<Can permission="(users.* || posts.*) && active"> {/* Grouped expressions */}
<Can permission="true"> {/* Always allow */}
<Can permission="false"> {/* Always deny */}
function TenantDashboard() {
const [tenantPermissions, setTenantPermissions] = useState([]);
useEffect(() => {
fetchTenantPermissions().then(setTenantPermissions);
}, []);
return (
<Can permission="tenant.admin || tenant.manager" permissions={tenantPermissions}>
<TenantControls />
</Can>
);
}
const featureFlags = ['feature.newUi', 'feature.advancedSearch'];
<Can permission="feature.newUi" permissions={featureFlags}>
<NewUserInterface />
</Can>;
<Can permission="(department.hr && level.manager) || admin.override">
<SensitiveEmployeeData />
</Can>
- Changelog - Version history and updates
Full TypeScript support with proper type definitions:
import type { CanProps, UsePermissionsReturn } from '@devwizard/laravel-react-permissions';
const MyComponent: React.FC<CanProps> = ({ permission, children }) => {
const { hasPermission }: UsePermissionsReturn = usePermissions();
return <Can permission={permission}>{children}</Can>;
};
- π¦ Package Size: 17.7kB (minified)
- ποΈ Gzipped: ~5.2kB
- π³ Tree Shakeable: β Import only what you use
- π± Runtime: Zero dependencies, React peer only
- β‘ Performance: Memoized pattern matching, optimized algorithms
# Only import what you need - tree shaking works perfectly
import { Can } from '@devwizard/laravel-react-permissions'; # ~3kB
import { usePermissions } from '@devwizard/laravel-react-permissions'; # ~2kB
import { withPermission } from '@devwizard/laravel-react-permissions'; # ~1kB
- β Modern Browsers: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
- β ES Modules: Native ESM support
- β CommonJS: Node.js compatibility
- β TypeScript: 5.x+ with strict mode
- React: 18.x, 19.x
- Inertia.js: 1.x, 2.x
- TypeScript: 5.x+
- Node.js: 16.x+
- Package Managers: npm 7+, yarn 1.22+, pnpm 6+
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
MIT License - see the LICENSE file for details.
IQBAL HASAN
- GitHub: @devwizardHq
- Organization: DevWizard
- Laravel team for the excellent
@can
directive inspiration - Spatie for the Laravel Permission package
- Inertia.js team for the seamless React-Laravel integration
- Zero runtime dependencies
- Tree-shakeable
- TypeScript native
- React 18/19 compatible
- Laravel convention compliant
Made with β€οΈ by DevWizard